Spring Security vs Apache Shiro

1. Oversigt

Sikkerhed er et primært problem i verden af ​​applikationsudvikling, især inden for virksomhedsweb- og mobilapplikationer.

I denne hurtige vejledning vi sammenligner to populære Java-sikkerhedsrammer - Apache Shiro og Spring Security.

2. En lille baggrund

Apache Shiro blev født i 2004 som JSecurity og blev accepteret af Apache Foundation i 2008. Indtil i dag har den set mange udgivelser, den seneste skrivning er 1.5.3.

Spring Security startede som Acegi i 2003 og blev indarbejdet i Spring Framework med sin første offentlige udgivelse i 2008. Siden starten har den gennemgået flere iterationer, og den nuværende GA-version, da den blev skrevet, er 5.3.2.

Begge teknologier tilbyder understøttelse af godkendelse og autorisation sammen med kryptografi og session management-løsninger. Derudover giver Spring Security førsteklasses beskyttelse mod angreb som CSRF og fixering af sessioner.

I de næste par sektioner vil vi se eksempler på, hvordan de to teknologier håndterer godkendelse og autorisation. For at holde tingene enkle bruger vi grundlæggende Spring Boot-baserede MVC-applikationer med FreeMarker-skabeloner.

3. Konfiguration af Apache Shiro

Lad os starte med at se, hvordan konfigurationer adskiller sig mellem de to rammer.

3.1. Maven afhængigheder

Da vi bruger Shiro i en Spring Boot-app, har vi brug for dens starter og shiro-kerne modul:

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

De nyeste versioner findes på Maven Central.

3.2. Oprettelse af et rige

For at erklære brugere med deres roller og tilladelser i hukommelsen er vi nødt til at oprette et rige, der udvider Shiro's JdbcRealm. Vi definerer to brugere - Tom og Jerry med henholdsvis roller USER og ADMIN:

offentlig klasse CustomRealm udvider JdbcRealm {private Map credentials = new HashMap (); private kortroller = nye HashMap (); private korttilladelser = ny HashMap (); {credentials.put ("Tom", "password"); credentials.put ("Jerry", "password"); roller.put ("Jerry", nyt HashSet (Arrays.asList ("ADMIN"))); roller.put ("Tom", nyt HashSet (Arrays.asList ("BRUGER")); permissions.put ("ADMIN", nyt HashSet (Arrays.asList ("LÆS", "SKRIV")); permissions.put ("USER", nyt HashSet (Arrays.asList ("LÆS")); }}

Dernæst er vi nødt til at tilsidesætte et par metoder for at muliggøre hentning af denne godkendelse og godkendelse:

@ Override beskyttet AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) kaster AuthenticationException {UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (userToken.getUsername () == null || userToken.getUsername (). isEmpty () ||! credentials.containsKey (userToken.getUsername ())) {kast ny UnknownAccountException ("Bruger findes ikke"); } returner nyt SimpleAuthenticationInfo (userToken.getUsername (), credentials.get (userToken.getUsername ()), getName ()); } @ Override beskyttet AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) {Sæt roller = ny HashSet (); Indstil tilladelser = nyt HashSet (); for (Objektbruger: principals) {prøv {roller.addAll (getRoleNamesForUser (null, (String) bruger)); tilladelser.addAll (getPermissions (null, null, roller)); } fange (SQLException e) {logger.error (e.getMessage ()); }} SimpleAuthorizationInfo authInfo = ny SimpleAuthorizationInfo (roller); authInfo.setStringPermissions (tilladelser); returner authInfo; } 

Metoden doGetAuthorizationInfo bruger et par hjælpemetoder til at få brugerens roller og tilladelser:

@ Override-beskyttet Sæt getRoleNamesForUser (Forbindelsesforbindelse, String-brugernavn) kaster SQLException {hvis (!roller.containsKey (brugernavn)) {kast ny SQLException ("Bruger findes ikke"); } returnere role.get (brugernavn); } @ Override-beskyttet Sæt getPermissions (Forbindelsesforbindelse, String-brugernavn, Samlingsroller) kaster SQLException {Set userPermissions = new HashSet (); for (String rolle: roller) {if (! permissions.containsKey (rolle)) {kast ny SQLException ("Roll eksisterer ikke"); } userPermissions.addAll (permissions.get (rolle)); } returnere brugerPermissioner; } 

Dernæst skal vi medtage dette CustomRealm som en bønne i vores boot-applikation:

@Bean public Realm customRealm () {returner ny CustomRealm (); }

Derudover har vi brug for en anden bønne for at konfigurere godkendelse til vores slutpunkter:

@Bean offentlig ShiroFilterChainDefinition shiroFilterChainDefinition () {DefaultShiroFilterChainDefinition filter = nyt DefaultShiroFilterChainDefinition (); filter.addPathDefinition ("/ home", "authc"); filter.addPathDefinition ("/ **", "anon"); returfilter; }

Her ved hjælp af en StandardShiroFilterChainDefinition eksempel specificerede vi, at vores /hjem slutpunkt kan kun tilgås af godkendte brugere.

Det er alt, hvad vi har brug for til konfigurationen, Shiro gør resten for os.

4. Konfiguration af forårssikkerhed

Lad os nu se, hvordan man opnår det samme om foråret.

4.1. Maven afhængigheder

For det første afhængighederne:

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

De nyeste versioner kan findes på Maven Central.

4.2. Konfigurationsklasse

Dernæst definerer vi vores Spring Security-konfiguration i en klasse Sikkerhedskonfig, forlænger WebSecurityConfigurerAdapter:

@EnableWebSecurity offentlig klasse SecurityConfig udvider WebSecurityConfigurerAdapter {@Override beskyttet ugyldig konfiguration (HttpSecurity http) kaster undtagelse {http .authorizeRequests (autorisér -> godkend .antMatchers ("/ index", "/ login"). PermitAll () .antMatchers ("/ hjem "," / logout "). godkendt () .antMatchers (" / admin / ** "). hasRole (" ADMIN ")) .formLogin (formLogin -> formLogin .loginPage (" / login ") .failureUrl (" /login fejl")); } @ Override beskyttet ugyldig konfiguration (AuthenticationManagerBuilder auth) kaster undtagelse {auth.inMemoryAuthentication () .withUser ("Jerry"). Adgangskode (passwordEncoder (). Kodning ("password")). Autoriteter ("LÆS", "WRITE") .roles ("ADMIN"). og () .withUser ("Tom") .password (passwordEncoder (). encode ("password")) .authorities ("READ") .roles ("USER"); } @Bean public PasswordEncoder passwordEncoder () {returner ny BCryptPasswordEncoder (); }} 

Som vi kan se, byggede vi en AuthenticationManagerBuilder modsætter sig at erklære vores brugere med deres roller og autoriteter. Derudover kodede vi adgangskoderne ved hjælp af en BCryptPasswordEncoder.

Spring Security giver os også dets HttpSikkerhed objekt til yderligere konfigurationer. For vores eksempel har vi tilladt:

  • alle for at få adgang til vores indeks og Log på sider
  • kun godkendte brugere til at indtaste hjem side og Log ud
  • kun brugere med ADMIN-rolle at få adgang til admin sider

Vi har også defineret understøttelse af formbaseret godkendelse til at sende brugere til Log på slutpunkt. Hvis login mislykkes, omdirigeres vores brugere til /login fejl.

5. Controllere og slutpunkter

Lad os nu se på vores webcontroller-kortlægninger for de to applikationer. Mens de bruger de samme slutpunkter, vil nogle implementeringer variere.

5.1. Slutpunkter til visning

For slutpunkter, der gengiver visningen, er implementeringerne de samme:

@GetMapping ("/") offentligt strengindeks () {returner "indeks"; } @GetMapping ("/ login") offentlig String showLoginPage () {returner "login"; } @GetMapping ("/ home") offentlig String getMeHome (Modelmodel) {addUserAttributes (model); vende hjem"; }

Både vores controllerimplementeringer, Shiro såvel som Spring Security, returnerer index.ftl på rodendepunktet, login.ftl på login-slutpunktet, og hjem.ftl på hjemmets slutpunkt.

Men definitionen af ​​metoden addUserAttributter ved /hjem slutpunkt vil variere mellem de to controllere. Denne metode introducerer den aktuelt loggede brugers attributter.

Shiro giver en SecurityUtils # getSubject for at hente strømmen Emne, og dens roller og tilladelser:

private ugyldige addUserAttributes (modelmodel) {Subject currentUser = SecurityUtils.getSubject (); Strengstilladelse = ""; hvis (currentUser.hasRole ("ADMIN")) {model.addAttribute ("rolle", "ADMIN"); } ellers hvis (currentUser.hasRole ("USER")) {model.addAttribute ("role", "USER"); } hvis (currentUser.isPermitted ("LÆS")) {tilladelse = tilladelse + "LÆS"; } hvis (currentUser.isPermitted ("OPSKRIV")) {tilladelse = tilladelse + "Opret"; } model.addAttribute ("brugernavn", currentUser.getPrincipal ()); model.addAttribute ("tilladelse", tilladelse); }

På den anden side giver Spring Security en Godkendelse objekt fra dens SecurityContextHolder'S kontekst til dette formål:

private ugyldige addUserAttributes (modelmodel) {Authentication auth = SecurityContextHolder.getContext (). getAuthentication (); hvis (auth! = null &&! auth.getClass (). er lig med (AnonymousAuthenticationToken.class)) {User user = (User) auth.getPrincipal (); model.addAttribute ("brugernavn", user.getUsername ()); Indsamlingsmyndigheder = user.getAuthorities (); for (GrantedAuthority autoritet: autoriteter) {hvis (autoritet.getAuthority (). indeholder ("BRUGER")) {model.addAttribute ("rolle", "BRUGER"); model.addAttribute ("tilladelser", "LÆS"); } ellers hvis (autoritet.getAuthority (). indeholder ("ADMIN")) {model.addAttribute ("rolle", "ADMIN"); model.addAttribute ("tilladelser", "LÆS SKRIV"); }}}}

5.2. POST login slutpunkt

I Shiro kortlægger vi de legitimationsoplysninger, som brugeren indtaster til en POJO:

offentlig klasse UserCredentials {privat streng brugernavn; privat strengadgangskode; // getters og setters}

Så opretter vi en BrugernavnPasswordToken for at logge brugeren eller Emne, i:

@PostMapping ("/ login") offentlig String doLogin (HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {Subject subject = SecurityUtils.getSubject (); if (! subject.isAuthenticated ()) {UsernamePasswordToken token = new UsernamePasswordToken (credentials.getUsername (), credentials.getPassword ()); prøv {subject.login (token); } fange (AuthenticationException ae) {logger.error (ae.getMessage ()); attr.addFlashAttribute ("fejl", "Ugyldige legitimationsoplysninger"); returner "omdirigering: / login"; }} returner "omdirigering: / hjem"; }

På forårssikkerhedssiden er dette bare et spørgsmål om omdirigering til hjemmesiden. Forårs loginproces håndteret af dets BrugernavnPasswordAuthenticationFilter, er gennemsigtig for os:

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

5.3. Admin-Only Endpoint

Lad os nu se på et scenarie, hvor vi skal udføre rollebaseret adgang. Lad os sige, at vi har en / admin slutpunkt, hvis adgang kun skal være tilladt for ADMIN-rollen.

Lad os se, hvordan man gør dette i Shiro:

@GetMapping ("/ admin") offentlig String adminOnly (ModelMap modelMap) {addUserAttributes (modelMap); Emne currentUser = SecurityUtils.getSubject (); hvis (currentUser.hasRole ("ADMIN")) {modelMap.addAttribute ("adminContent", "kun admin kan se dette"); } vende hjem"; }

Her udpakkede vi den aktuelt loggede bruger, kontrollerede om de havde ADMIN-rollen og tilføjede indhold i overensstemmelse hermed.

I Spring Security er der ikke behov for at kontrollere rollen programmatisk, vi har allerede defineret, hvem der kan nå dette slutpunkt i vores Sikkerhedskonfig. Så nu er det bare et spørgsmål om at tilføje forretningslogik:

@GetMapping ("/ admin") offentlig String adminOnly (HttpServletRequest req, Model model) {addUserAttributes (model); model.addAttribute ("adminContent", "kun admin kan se dette"); vende hjem"; }

5.4. Logout slutpunkt

Lad os endelig implementere logout-slutpunktet.

I Shiro ringer vi simpelthen Emne # logout:

@PostMapping ("/ logout") offentlig Log ud af streng () {Emneemne = SecurityUtils.getSubject (); subject.logout (); returner "omdirigering: /"; }

Til foråret har vi ikke defineret nogen kortlægning til logout. I dette tilfælde starter standardlogoutmekanismen, som automatisk anvendes, siden vi udvidede WebSecurityConfigurerAdapter i vores konfiguration.

6. Apache Shiro vs Spring Security

Nu hvor vi har set på forskellene i implementeringen, lad os se på et par andre aspekter.

Med hensyn til samfundsstøtte er den Spring Framework har generelt et stort samfund af udviklere, aktivt involveret i dets udvikling og anvendelse. Da Spring Security er en del af paraplyen, skal den nyde de samme fordele. Selvom Shiro er populær, har han ikke så enorm støtte.

Med hensyn til dokumentation er Spring igen vinderen.

Der er dog lidt af en indlæringskurve forbundet med Spring Security. Shiro er derimod let at forstå. For desktop-applikationer, konfiguration via shiro.ini er jo nemmere.

Men igen, som vi så i vores eksempler på uddrag, Spring Security gør et godt stykke arbejde med at holde forretningslogik og sikkerhedadskille og tilbyder virkelig sikkerhed som en tværgående bekymring.

7. Konklusion

I denne vejledning vi sammenlignede Apache Shiro med Spring Security.

Vi har lige græsset overfladen af, hvad disse rammer har at tilbyde, og der er meget at udforske yderligere. Der er en hel del alternativer derude som JAAS og OACC. Alligevel ser Spring Security med sine fordele ud til at vinde på dette tidspunkt.

Som altid er kildekoden tilgængelig på GitHub.