Spring Security

Izvor: SIS Wiki
Skoči na: orijentacija, traži

Članovi: Leon Palaić --Lpalaic 23:08, 12. studenog 2015. (CET) Leon Palaić

Sadržaj


Uvod

Razvoj sigurnih web aplikacija nije jednostavan posao stoga se razvila potreba za razvojnim okvirima koji će olakšati taj posao. Jedan takav razvojni okvir je Spring Security. Spring Security je razvojni okvir koji pruža mogućnosti autentifikacije, autorizacije te sprječavanje određenih napada kao što su session fixation i cross site request forgery unutar aplikacija baziranih na JVM-u (Java, Groovy, Scala). Važno je napomenuti da ovaj razvojni okvir ne rješava sve probleme iz OWASP top 10 problema. Unutar ovog projekta razvit ćemo jednostavnu aplikaciju kako bi pokazali funkcionalnosti Spring Security-a. Cilj ovog projekta je pružiti niz primjera korištenja i konfiguriranja Spring Security-a kako čitatelji ne bi sami morali trošiti vrijeme na istraživanje i razumijevanje ovog razvojnog okvira.

Tehnologije

Za potrebe razvoja primjera koji koriste Spring Security okvir, koristit ćemo Java programski jezik sa Spring Frameworkom kako bi ubrzali implementaciju, Hibernate kao ORM za manipuliranje s bazom podataka te MySQL kao bazu podataka.

ERA model

Era model

Kako bi implementirali primjere napravljen je ERA model kao u prilogu. Model je jednostavan te će se sastojati od tablice role, person i test_data. Tablica role služi kao šifarnik za korisničke ovlasti u sustavu, tablica person za podatke o korisniku te tablica test_data za podatke koje će korisnici unositi.

Spring Security

Spring Security ima dva glavna područja na koje je usmjeren, a to su: autentifikacija i autorizacija. Spring je sam po sebi aplikacijski kontejner za razvoj aplikacija. Spring dolazi u formi jednog ili više aplikacijskih konteksta. Aplikacijski kontekst služi za konfiguraciju objekata koji izvršavaju logiku, dohvaćaju podatke, odgovaraju na HTTP zahtjeve, konfiguriranje komunikacije između objekata te mnoge druge stvari. Zapravo možemo reći da je aplikacijski kontekst opisnik aplikacije koji sadrži sve potrebno kako bi aplikacija radila. S obzirom da je Spring Security razvojni okvir za sigurnost on koristi svoj sigurnosni aplikacijski kontekst. Glavni objekt s kojim Spring Security radi je SecurityContextHolder koji pohranjuje podatke sigurnosnog aplikacijskog konteksta. Sigurnosni aplikacijski kontekst sadrži objekte za autentifikaciju, autorizaciju te ostale objekte s aspekta sigurnosti aplikacije. Više o objektima sigurnosnog aplikacijskog konteksta u poglavljima o autentifikaciji i autorizaciji. Uz sigurnosni kontekst važno je znati što su filteri u Java svijetu i kako oni rade. Filteri služe za presretanje i preusmjeravanje zahtjeva i odgovora prema određenim resursima. Također filteri mogu modificirati, odbijati, preusmjeravati ili prosljeđivati zahtjeve i odgovore. Spring Security se sastoji od niza sigurnosnih filtera koji obrađuju zahtjeve i odgovore prema resursima aplikacije.

Moduli Spring Security-a

Spring Security se sastoji od niza modula različitih funkcionalnosti. Moduli Spring Security-a su:

  1. Core modul - služi za autentifikaciju te sadrži klase i sučelja za autorizaciju i prava pristupa objektima.
  2. Crypto modul - pruža mogućnosti simetrične enkripcije, generiranje ključeva te kodiranje znakova
  3. Security Remoting modul - služi za intefraciju Spring Security-a s Spring Remoting modulom
  4. Web modul - sadrži sve Serverlet ovisnosti, filtere i ostalu infrastrukturu kako bi usluge autentifikacije i kontrole pristupa bile moguće
  5. Config modul - sadrži prostor funkcijskih imena kako bi mogli konfigurirati Spring Security putem XML-a.
  6. LDAP - sadrži potrebnu infrastrukturu za korištenje LDAP protokola.
  7. ACL (access control list) modul - služi za primjenjivanje sigurnosnih konfiguracija resursima unutar aplikacije
  8. CAS modul - omogućava CAS single sign on protokol
  9. OpenID modul - omogućava autentifikaciju putem OpenID protokola

Autentifikacija

Za početak prikazat ćemo postupak rada unutar Spring Security razvojnog okruženja. Sam postupak se sastoji od niza koraka:

  1. Korisnik unosi korisničko ime i lozinku
  2. Sustav verificira korisničko ime i lozinku
  3. Dohvaćaju se korisnikove informacije (prava pristupa itd.) i pohranjuju u sigurnosni kontekst
  4. Korisnik obavlja akcije u sustavu te AC (acess control) mehanizam provjerava dozvole korisnika za određene operacije

Sama autentifikacija korisnika odvija se unutar prva tri koraka prethodnog postupka. Kako bi se korisnik autentificirao unutar sustava Spring Security mora pokupiti same informacije o korisniku. Za potrebe dohvaćanja podataka o korisnicima Spring Security ima definirano sučelje UserDetails koje služi kao poveznica između baze podataka korisnika i sigurnosnog konteksta. Kako bi se sigurnosnom kontekstu pružili podatci o korisniku moramo definirati način kako povezati UserDetails objekt i sigurnosni kontekst. Spring Security za tu potrebu ima definirano sučelje UserDetailsService. UserDetailsSerivce će koristiti UserDetails za instanciranje objekta s korisničkim podatcima te će se na temelju tih podataka konstruirati Authentication objekt koji će se spremiti u sigurnosni kontekst. Prilikom unosa korisničkog imena i lozinke Spring Security gradi Authentication objekt te ga predaje AuthenticationManageru koji taj objekt prosljeđuje AuthenticationProvideru koji obavlja samu autentifikaciju. Nakon što AuthenticationProvider obavi autentifikaciju vraća AuthenticationToken AutjenticationManageru ukoliko je korisnik uspješno autentificiran ili podiže iznimku koju obrađuje AuthenticationManager. U slučaju da je korisnik uspješno autentificiran podatci o korisniku se pohranjuju unutar sigurnosnog konteksta putem Authentication objekta. Sada kada znamo nešto više o samom procesu možemo krenuti konstruirati kostur našeg programa. Za početak ćemo unutar application.properties-a definirati podatke za spajanje na bazu podataka, port na kojem ćemo pokrenuti server.

server.port=8080 
spring.datasource.url: jdbc:mysql://localhost:3306/dtp 
spring.datasource.username: username
spring.datasource.password: pasword
spring.datasource.driverClassName: com.mysql.jdbc.Driver 

Kako bi dodali Spring Security u projekt potrebno je u pom.xml dodati sljedeću ovisnost.

<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Security unutar svog Crypto modula sadrži simetrične načine kriptiranja korisnikovih lozinki. S obzirom da Crypto modul ne sadrži implementacije određenih hash funckija osim BCrypt-a dodat ćemo implementaciju PBKDF2 unutar našeg projekta. Unutar klase implementirati ćemo funkcije authenthicate koja će unesenu lozinku hashirati zajedno sa saltom spremljene lozinke te ju zatim usporediti sa spremljenom lozinkom, getEncryptedPassword koja vraća generiranu lozinku u heksadecimalnom zapisu, generateSalt funkciju koja će generirati salt te funkcije. Važno je napomenuti da je preporuka da se svake dvije godine broj iteracija PBKDF2 algoritma povećava za duplo. Treba napomenuti da SecretKeyFactory vraća lozinku u byte-ovima, a mi smo naveli da u bazi spremamo lozinke u formatu varchar stoga ćemo ju pretvoriti u heksadecimalni format.

public final class PBKDF2 {
    
    public static boolean authenticate(String attemptedPassword, String encryptedPassword, String salt)
    throws NoSuchAlgorithmException, InvalidKeySpecException {

     byte [] encryptedPasswordArray = fromHex(encryptedPassword);
    
     String encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);

     byte [] attemptedEncryptedPasswordArray = fromHex(encryptedAttemptedPassword);
     
     return Arrays.equals(encryptedPasswordArray, attemptedEncryptedPasswordArray);
 }

    public static String getEncryptedPassword(String password, String salt)
    throws NoSuchAlgorithmException, InvalidKeySpecException {

     byte [] saltArray = fromHex(salt);   
        
     String algorithm = "PBKDF2WithHmacSHA256";

     int derivedKeyLength = 256;

     int iterations = 100000;

     KeySpec spec = new PBEKeySpec(password.toCharArray(), saltArray, iterations, derivedKeyLength);

     SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);

     return toHex(f.generateSecret(spec).getEncoded());

    }

    public static String generateSalt() throws NoSuchAlgorithmException {

     SecureRandom random = new SecureRandom();
     byte[] salt = new byte[32];
     random.nextBytes(salt);

     return toHex(salt);

    }
    
    
}

S obzirom da ćemo koristiti nasumični salt za svakog korisnika prilikom autentifikacije napravit ćemo svoje sučelje koje će naslijediti UserDetails i dodati funkciju koja će vraćati salt korisnika.

public interface UserSaltDetails extends UserDetails {
    
    public String getUserSalt();
    
}

Kako bi dohvaćali podatke kreirat ćemo PersonRepository koji će nam služiti kao DAO za tablicu person iz naše baze.

@Repository
@Table(name="person")
public interface PersonRepository extends JpaRepository<Person, String> {
    
   
    public Person findByIdPerson(long id);
    
    public Person findByCredentialsUsername(String username);
    
}

Sada kada smo postavili repozitorij korisnika te definirali svoj UserDetails koji smo nazvali UserSaltDetails vrijeme je da napravimo UserDetailsService. Naš UserDetailsService nazvat ćemo PersonDetailsService koji će naslijediti UserDetailsService te ćemo preopteretiti funkciju loadUserByUsername koja će nam vratiti podatke o korisniku na temelju korisnikovog korisničkog imena. Nadalje ćemo definirati da nam Spring dodijeli instancu PersonRepositorya unutar PersonDetailsService-a. Također preostaje nam definirati klasu koja će implementirati naše metode iz UserSaltDetails za te potrebe ćemo napraviti unutarnju klasu PersonRepositoryUserDetails koja će naslijediti klasu Person te implementirati metode iz sučelja.

@Service
public class PersonDetailsService implements UserDetailsService {
    
    private PersonRepository personRepository;

    @Autowired
    public PersonDetailsService(PersonRepository personRepository) {
            this.personRepository = personRepository;
    }

    @Override
    public UserSaltDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Person user = personRepository.findByCredentialsUsername(username);
            if (user == null) {
                    throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
            }
            
            return new PersonRepositoryUserDetails(user);
    }

    private final  class PersonRepositoryUserDetails extends Person implements UserSaltDetails {

            
           
            private PersonRepositoryUserDetails(Person user) {

                    super(user);
      
            }

            @Override
            public String getUsername() {
                    return getCredentials().getUsername();
                            
            }

            @Override
            public boolean isAccountNonExpired() {
                    return true;
            }

            @Override
            public boolean isAccountNonLocked() {
                    return true;
            }

            @Override
            public boolean isCredentialsNonExpired() {
                    return true;
            }

            @Override
            public boolean isEnabled() {
                    return true;
            }

            @Override
        public String getPassword() {
            return getCredentials().getPassword();
        }
        
        @Override
        public String getUserSalt(){
            return getSalt();
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            
            HashSet authorities= new HashSet<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority(getRole().getName()));
            return authorities;
            
        }

    }
    
}

Preostaje nam još definirati AuthenticationProvider-a koji će obavljati autentifikaciju korisnika. Ranije smo naveli da AuthenticationManager prosljeđuje Authentication objekt Provideru. Stoga ćemo definirati Providera koji će koristiti PersonDetailsService za dohvaćanje podataka o korisniku te PBKDF2 za provjeravanje unesene lozinke i lozinke korisnika. Ukoliko je lozinka točna Provider će vratiti Token sa objektom UserSaltDetails, korisničkom lozinkom te ovlastima koje korisnik ima u sustavu. O korisničkim ovlastima bit će govora unutar odjeljka o autorizaciji.

@Component
public class PBKDF2AuthProvider implements AuthenticationProvider{
    
     @Autowired
     private PersonDetailsService userService;

    @Override
    public Authentication authenticate(Authentication a) throws AuthenticationException {
        
         
             String username = a.getName();
              
           
             
             if( username == null)
                 throw new BadCredentialsException("Username not found.");
             
             String password = (String) a.getCredentials();
            
             
             
             if( password == null )
                 throw new BadCredentialsException("Password not found.");
             
          
             
             UserSaltDetails user = userService.loadUserByUsername(username);
             
 

             boolean isAuthenticated = false; 
             
            try {
                
                isAuthenticated = PBKDF2.authenticate(password, user.getPassword(),user.getUserSalt());

     
            } catch (NoSuchAlgorithmException ex) {
                Logger.getLogger(PBKDF2AuthProvider.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InvalidKeySpecException ex) {
                Logger.getLogger(PBKDF2AuthProvider.class.getName()).log(Level.SEVERE, null, ex);
            }
             
          if(!isAuthenticated)
                 throw new BadCredentialsException("Wrong password.");
          else
                 Logger.getLogger("Auth").log(Level.INFO,
                         "Authenticated");

         return new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAuthorities());
       
    }

    @Override
    public boolean supports(Class<? extends Object> authentication) {
         return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
    
}

Nakon što smo izradili infrastrukturu za autentifikaciju potrebno je omogućiti Spring Security unutar projekta. Kako bi omogućili Spring Security unutar projekta potrebno je izraditi klasu za konfiguraciju koja nasljeđuje WebSecurityConfigurerAdapter te pomoću anotacije @EnableWebSecurity omogućiti Spring Security. Kreirat ćemo klasu ConfSecurity koja će imati metode configureGlobal i configure. Unutar metode configureGlobal definirat ćemo AuthenticationManagera koji će koristiti AuthenticationProvidera kojeg smo ranije napravili. Te unutar metode configure definirat ćemo autorizacijske postavke naše aplikacije. Ovu metodu ćemo nadopuniti kasnije kada bude govora o autorizaciji.

@Component
@EnableWebSecurity
public class ConfSecurity extends WebSecurityConfigurerAdapter{

    @Autowired
    PBKDF2AuthProvider authenticationProvider;
   
    @Autowired
          public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

               auth.authenticationProvider(authenticationProvider);

          }	
          
  

      @Override
      protected void configure(HttpSecurity http) throws Exception {


  }
}

Spring Security pruža nekoliko mogućnosti definiranja UserDetailsService-a ukoliko ne želimo raditi samostalnu implementaciju. Jedan takav način je da okvir koristi JDBC kao izvor podataka. Ovaj način zaobilazi korištenje ORM-a te se direktno upitima na bazu dohvaćaju korisnički podatci bez dodatnog sloja ORM-a. Ukoliko Vaša aplikacija ne koristi ORM ovo je način za vas. U nastavku slijedi primjer koji koristi izvor podataka definiran unutar application.properties-a. Te na temelju UsernameQuerya-a dohvaća podatke o korisniku te zatim putem authoritiesByUsernameQuery dohvaća ovlasti korisnika.

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.jdbcAuthentication()
			.dataSource(dataSource)
			.withDefaultSchema()
                        .usersByUsernameQuery(getUserQuery())
                        .authoritiesByUsernameQuery(getAuthoritiesQuery());;
			
}
private String getUserQuery() {

        return "SELECT  username,  password "
                + "FROM person "
                + "WHERE username = ?";

}
    
   private String getAuthoritiesQuery(){
         return "SELECT  p.username, r.name "
                + "FROM person p join role r  ON p.role_id = r.id_role"
                + "WHERE p.username = ?";
}

Ukoliko imamo slučaj da prototipiramo aplikaciju te želimo izbjeći kompleksnost ranije navedenih načina, ovaj okvir omogućava In-Memory autentifikaciju. Koja je pogodna ukoliko moramo izdati brzo prototip same aplikacije. Primjer je dan u nastavku:

@Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth
      .inMemoryAuthentication()
        .withUser("user").password("password").roles("USER");
  }


  

Ukoliko ne želimo definirati vlastite resurse za kodiranje i dekodiranje lozinki možemo koristiti Crypto modul. Crypto modul pruža kodiranje lozinki putem hash algoritama kao što su MD5,SHA-1,SHA-256 te BCrypt algoritma. U nastavku je dan primjer korištenja BCrypt algoritma:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

	auth
		.jdbcAuthentication()
			.dataSource(dataSource)
			.withDefaultSchema()
			.withUser("user").password("//HASHLOZINKE").roles("USER").and()
			.withUser("admin").password("//HASHLOZINKE").roles("USER", "ADMIN")
                        .and().passwordEncoder(encoder);

}

Autorizacija

Unutar razvojnog okvira sve operacije vezane uz prava pristupa zaduženo je sučelje AccessDecisionManager. AccessDecisionManager unutar svoje metode decide koja prima Authentication objekt te na temelju njegovih prava dozvoljava pristup ili zabranjuje. Ranije smo definirali da prilikom uspješne autentifikacije se kreira Authentication objekt s podatcima o korisniku. Jedan takav podatak su i prava pristupa samog korisnika. Prava pristupa korisnika dobivena su ranije definiranom metodom unutar UserDetails sučelja. Referencirat ćemo se na klasu koju smo ranije napravili te prikazati metodu u navedenom primjeru:

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            
            HashSet authorities= new HashSet<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority(getRole().getName()));
            return authorities;
            
        }

Kako bismo objasnili kako autorizacija radi unutar razvojnog okvira moramo znati što su interceptori. Interceptori su klase čije se metode pozivaju u konjukciji sa metodama klasa kojima je interceptor pridružen. Unutar Spring Security-a svi objekti koji su zaštićeni pravima pristupa dobivaju svoju interceptorsku klasu koja je podklasa AbstractSecurityInterceptor-a. Stoga ukoliko se poziva metoda zaštićenog objekta pozvat će se i pripadna metoda iz interceptora te klase. Unutar razvojnog okvira interceptori pozivaju AccessDecisionManager-a da donese odluku o konačnoj odluci je li korisnik ima pravo pristupa navedenom resursu. Naravno ovo se odvija na navedeni način ukoliko nismo implementirali svoju proceduru. Kako bi konfigurirali prava pristupa u našem projektu unutar metode configure definirat ćemo resurse te prava pristupa tim resursima. Spring Security konfiguracija postavlja se putem HttpSecurity objekta. Primjer konfiguracije za našu aplikaciju:

@Override
      protected void configure(HttpSecurity http) throws Exception {

          http
                  .sessionManagement()
                  .maximumSessions(1);
          
          http
                  .csrf().disable()
                  .authorizeRequests()
                  .antMatchers("/admin-page.html").access("hasRole('ROLE_ADMIN')")
                  .antMatchers("/user-page.html").access("isAuthenticated()")
                  .antMatchers("/index.html","/register.html","spring-security.css").permitAll()
                  .antMatchers("/register.html").permitAll()
                  .and()
                  .formLogin().loginPage("/login.html")
                  .usernameParameter("username")
                  .passwordParameter("password")
                  .failureUrl("/login-error.html")
                  .defaultSuccessUrl("/user-page.html")
                  .and()
                  .logout().logoutUrl("/logout")
                  .logoutSuccessUrl("/index.html")
                  .and().exceptionHandling().accessDeniedPage("/acces-denied-page.html");
                  
                  
           

  }

Unutar primjera definirali smo putanje te zaštitu samih tih putanja. Definirali smo da pravo pristupa administratorskoj stranici imaju samo administratori, korisničkoj stranici mogu pristupiti svi autentificirani korisnici te da pravo pristupa početnoj, registracijskoj, css dokumetnu i login stranici imaju svi korisnici pa i oni ne autentificirani. Ukoliko se ne definira vlastita login stranica Spring Security će koristiti svoju defaultnu stranicu za login. Stoga smo u ovom primjeru definirali svoju login stranicu sa parametrima koje korisnik šalje, stranicom u slučaju neuspjeha pri loginu, stranicu na koju se preusmjerava prilikom uspješnog login-a. Također definirali smo logout putanju, stranicu na koju se preusmjerava prilikom uspješnog logouta te stranicu koja će se prikazati prilikom pristupa zabranjenih resursa. Za sad smo zabranili Cross Site Request Forgery zaštitu nju ćemo obraditi u posebnom ulomku. Kao što vidimo putem antMatchers-a štitimo putanje do pojedinih resursa aplikacije. Spring Securiy uz ovaj način pruža i način štićenja resursa putem anotacija. Tako možemo štiti klase, sučelja te njihove metode. Kako bi uključili ovaj način moramo dodati anotaciju @EnableGlobalMethodSecurity (prePostEnabled = true) iznad naše ConfSecurity klase. U nastavku prikazat će se nekoliko primjera štićenja reusursa na ovaj način. Postoji nekoliko načina štićenja na ovaj način, a to su @PreAuthorize i @PostAuthorize odnosno prije i nakon autorizacije. U nastavku zaštitit ćemo metodu kontrolera koja vraća podatke o pojedinom korisniku te metodu DAO sučelja koja vraća podatke o osobi putem id-a osobe. Možemo vidjeti iznad same metode anotaciju @PreAuthorize te unutar nje izraz koji se mora zadovoljiti kako bi se metoda pozvala. Dozvoljeni izrazi unutar anotacija su izrazi pisani putem EL (Expression Language) jezika. Navedeni izraz dozvolit će pristup metodi ukoliko je korisnik autentificiran te ukoliko je njegov id jednak traženom id-u. Na ovaj način dozvolili smo pristup samo vlastitim podatcima.

 /**
     * gets user with specified id
     * @param id id of user
     * @return person info with HTTP 200 on success or HTTP 404 on fail
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @PreAuthorize("isAuthenticated() and principal.idPerson == #id")
    public ResponseEntity<Person> retrieveById(@PathVariable("id") long id) {
        
        Logger.getLogger("PersonController.java").log(Level.INFO,
                "GET on /person/" + id + " -- ");
        
        Person found = this.personRepository.findByIdPerson(id);
        if(found != null) {
            Logger.getLogger("PersonController.java").log(Level.INFO,
                    "User found for id " + id + ", returning " + found.toString());
            return new ResponseEntity(found, HttpStatus.OK);
        } else {
            Logger.getLogger("PersonController.java").log(Level.WARN,
                    "No user found for id " + id);
            return new ResponseEntity(HttpStatus.NOT_FOUND);
        }
        
    }

Osim štićenja metoda klasa možemo štiti i metode sučelja. Primjer je dan u nastavku:

@Repository
@Table(name="person")
public interface PersonRepository extends JpaRepository<Person, String> {
    
    @PreAuthorize("isAuthenticated() and principal.IdPerson = #id")
    public Person findByIdPerson(long id);
    
    public Person findByCredentialsUsername(String username);
    
}

CSRF

Cross site request forgery zaštita je automatski dodana unutar razvojnog okvira ukoliko se ne isključi kao što smo napravili u prošlom ulomku. Kako bismo uključili CSRF zaštitu eliminirat ćemo linije iz configure metode. Te će nam metoda sada izgledati kao u nastavku:

      @Override
      protected void configure(HttpSecurity http) throws Exception {

          http
                  .sessionManagement()
                  .maximumSessions(1);
          
          http
                  .authorizeRequests()
                  .antMatchers("/admin-page.html").access("hasRole('ROLE_ADMIN')")
                  .antMatchers("/user-page.html").access("isAuthenticated()")
                  .antMatchers("/index.html","/register.html","spring-security.css").permitAll()
                  .antMatchers("/register.html").permitAll()
                  .and()
                  .formLogin().loginPage("/login.html")
                  .usernameParameter("username")
                  .passwordParameter("password")
                  .failureUrl("/login-error.html")
                  .defaultSuccessUrl("/user-page.html")
                  .and()
                  .logout().logoutUrl("/logout")
                  .logoutSuccessUrl("/index.html")
                  .and().exceptionHandling().accessDeniedPage("/acces-denied-page.html")
                  .and().headers()
                  .xssProtection();
                  
         

  }

pring Security pretpostavlja da se stranice generiraju na poslužiteljskoj strani kao što je slučaj kod JSP-a, stoga ovo nije dovoljno jer naše stranice se ne generiraju na poslužiteljskoj strani. Ukoliko se razvija SPA, REST servis ili arhitektura koja nije vezana uz standardne Java EE aplikacije potrebno je definirati filter koji će prilikom zahtjeva korisnika vraćati CSRF token u HTTP zaglavlju. S obzirom da unutar Maven repozitorija postoji implementacija takvog filtera samo ćemo dodati ovisnost unutar našeg pom.xml-a kako bismo ga mogli koristiti. Da bismo dodali navedeni filter moramo dodati sljedeću ovisnost:

<dependency>
    <groupId>com.allanditzel</groupId>
    <artifactId>spring-security-csrf-token-filter</artifactId>
    <version>1.1</version>
</dependency>

Zatim ćemo dodati sljedeće linije koda kako bismo registrirali filter u našoj aplikaciji.

           CsrfTokenResponseHeaderBindingFilter csrfTokenFilter = new CsrfTokenResponseHeaderBindingFilter();    
           http.addFilterAfter(csrfTokenFilter, CsrfFilter.class);

Nakon što smo dodali filter u našu aplikaciju HTTP zaglavlja će izgledati kao na slici CSRF zaglavlje.

CSRF zaglavlje











Kako bismo slali zahtjeve moramo uključiti CSRF token unutar istih. Da bismo dohvatili CSRF token moramo ga dohvatiti iz HTTP zaglavalja odgovora servera. U nastavku je primjer uključivanja tokena unutar login forme:

$.ajax({
                type: 'GET',
                url: '/index.html'
 
            }).done(function (data, textStatus, jqXHR) {
            var csrfToken = jqXHR.getResponseHeader('X-CSRF-TOKEN');
            if (csrfToken) {
                $('[name="_csrf"]').val(csrfToken);
            }
            }).fail(function (jqXHR, textStatus, errorThrown) {
                console.log(errorThrown);
            });
  

Unutar funkcije hvatamo CSRF token te njegovu vrijednost dodajemo inputu forme pod imenom _csrf.

SSL

Spring Security omogućava komunikaciju između poslužitelja i klijenata putem SSL protokola. Kako bismo postavili SSL protokol unutar našeg projekta potrebno je postaviti Tomcat, izgenerirati certifikat za poslužitelja i SSL je spreman za korištenje. Za početak ćemo kreirati keystore sa certifikatom za našeg poslužitelja. Key store pohranit ćemo unutar našeg projekta.

Generiranje key store-a














Nakon što smo izgenerirali key store potrebno je unutar application.properties-a dodati isti te promijeniti port na kojem se pokreće Tomcat. I postupak za navedeno ćemo učiniti kao u nastavku:

server.port=8443
server.ssl.key-store=classpath:paz.keystore
server.ssl.key-store-password= // 
server.ssl.key-password= //

OAuth2

Kako bi implementirali OAuth2 unutar Spring Security-a moramo znati osnove o samom protokolu. Kako bi znali kako sam protokol radi moramo znati koje sve uloge postoje unutar samog protokola. OAuth definira 4 uloge, a to su:

  1. Resource Owner (Vlasnik resursa) - strana koja želi podijeliti resurse. Na primjer osoba želi podijeliti svoje fotografije ili aplikacija koja poslužuje određene podatke
  2. Resource Server (Poslužitelj resursa) - kao što samo ime govori ova uloga se odnosi na poslužitelja koji poslužuje resurse Resource Owner-a
  3. Client (Klijent) - aplikacija koja zahtjeva pristup resursima koji se poslužuju na Resource Server-u
  4. Authorization Server (Autorizacijski poslužitelj) - služi za autorizaciju klijentskih aplikacija koje žele pristupiti određenom resursu.

Tok protokola je definiran na slici u nastavku.
Oauth2.png

Sada kada znamo uloge i sam tok protokola znamo što sve trebamo implementirati. Kao primjer implementacije OAuth protokola poslužiti ću se projektom napravljenim na Ready Steady Code natjecanju. Sada kada znamo kako autentifikacija radi unutar Spring Security-a za potrebe autentifikacije korisnika koristit ćemo zadanog AuthenticationProvidera Spring Security-a te vlastiti UserDetailsService kojeg smo ranije definirali. Vrijeme je da definiramo komponente potrebne za OAuth2 protokol. Kreirat ćemo OAuthServerConfiguration klasu u kojoj ćemo definirati poslužitelj resursa, autorizacijski poslužitelj, repozitorij tokena te definirati koje klijentske aplikacije mogu pristupiti našem server-u. U ovom primjeru resursi koji su zaštićeni bit će putanje do kontrolera REST servisa. Kako bi definirali resurse unutar Spring Secuirty-a moramo definirati njihov jedinstveni identifikator te unutar poslužitelja resursa registrirati resurs putem identifikatora te konfigurirati postavke autorizacije za registrirani resurs. Važno je napomenuti da na ovaj način možemo definirati više štićenih resursa. Kako bi omogućili poslužitelja resursa moramo kreirati klasu te ju anotirati sa anotacijom @EnableResourceServer te unutar nje navesti ranije navedene postavke. Primjer konfiguriranja dan je u nastavku.


    private static final String RESOURCE_ID = "rsc-rest";
    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends
                    ResourceServerConfigurerAdapter {

            @Override
            public void configure(ResourceServerSecurityConfigurer resources) {
                    resources
                            .resourceId(RESOURCE_ID);
            }

            @Override
            public void configure(HttpSecurity http) throws Exception {
                    http
                            .authorizeRequests()
                                    .antMatchers("/person/signup").anonymous()
                                    .antMatchers("/game/*").authenticated()
                                    .antMatchers("/team/*").authenticated()
                                    .antMatchers("/maps/*").authenticated()
                                    .antMatchers("/notification/*").authenticated()
                                    .antMatchers("/mapsobstacles/*").authenticated()
                                    .antMatchers("/person/*").authenticated();
            }

Nadalje potrebno je definirati autorizacijskog poslužitelja. Unutar autorizacijskog poslužitelja moramo definirati upravljanje i spremanje tokena, dodijeliti AuthenticationManagera koji će obavljati autentifikaciju, definirati pristupne točke za dohvaćanje tokena te definirati klijentske aplikacije koje će koristiti autorizacijskog poslužitelja. Za početak ćemo kreirati klasu AuthorizationServerConfiguration te ju anotirati sa anotacijom @EnableAuthorizationServer kako bi registrirali autorizacijskog poslužitelja. S obzirom da je aplikaciju bilo potrebno implementirati unutar 24 sata koristio sam InMemoryTokenStore za spremanje tokena kako bi razvoj bio što brži.

@Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends
                    AuthorizationServerConfigurerAdapter {

            private TokenStore tokenStore = new InMemoryTokenStore();

            
}

Nadalje potrebno je postaviti upravljanje tokena. Kako bi postavili upravljanje tokenima unutar razvojnog okvira koristit ćemo zadani servis Spring Security-a pod imenom DefaultTokenService. Ovaj servis se brine o kreiranju pristupnih tokena, spremanjem tokena i svih ostalih operacija vezanih uz tokene. Servisu ćemo dodijeliti ranije kreirani TokenStore te ćemo metodu iz primjera dodati u ranije spomenutu klasu.

            @Bean
            @Primary
            public DefaultTokenServices tokenServices() {
                    DefaultTokenServices tokenServices = new DefaultTokenServices();
                    tokenServices.setSupportRefreshToken(true);
                    tokenServices.setTokenStore(this.tokenStore);
                    return tokenServices;
            }

Sada kada smo definirali način za upravljanje tokenima i spremanje potrebno je registrirati pristupne točke za autorizaciju korisnika te dohvaćanje tokena. Za te potrebe koristit ćemo zadanu putanju Spring Security-a:

  1. /oauth/token - putanja za dohvaćanje pristupnih tokena

Također za zadanu putanju definirat ćemo da koristi zadanog AuthenticationProvidera Spring Security-a te vlastiti UserDetailsService kojeg smo ranije definirali. Primjer implementacije::

            @Autowired
            @Qualifier("authenticationManagerBean")
            private AuthenticationManager authenticationManager;

            @Autowired
            private PersonDetailsService personDetailsService;

            @Override
            public void configure(AuthorizationServerEndpointsConfigurer endpoints)
                            throws Exception {
                    endpoints
                            .tokenStore(this.tokenStore)
                            .authenticationManager(this.authenticationManager)
                            .userDetailsService(personDetailsService);
            }

ZZa kraj nam preostaje definirati klijenta koji će autorizirati klijentske aplikacije. To ćemo napraviti unutar metode configure u kojoj ćemo definirati klijentsku aplikaciju, kojem resursu aplikacija ima pristup, kojim načinom korisnici dobivaju pristupne tokene te koje ovlasti ima nad resursom.

@Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                    clients
                            .inMemory()
                                    .withClient("angular")
                                            .authorizedGrantTypes("password")
                                            .authorities("ROLE_USER","ROLE_ADMIN")
                                            .scopes("read", "write")
                                            .resourceIds(RESOURCE_ID);
                                            
            }

O Auth2 podržava nekoliko načina razmijene pristupnih tokena. Iz primjera možemo vidjeti da smo definirali klijenta pod imenom angular sa metodom authorizedGrantTypes ("password") definirali smo da pristupne tokene dobiva putem lozinke i korisničkog imena korisnika. Zatim smo definirali da role klijenta mogu biti USER i ADMIN te prava pristupa klijenta su read i write. Nakon ove konfiguracije postavili smo poslužiteljsku stranu za ovaj protokol. Te sada klijentske aplikacije mogu putem putanje /oauth/token dobiti svoj pristupni token. Primjer POST zahtjeva klijentske aplikacije za pristupnim tokenom:

https://localhost:8443/oauth/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID

Nakon što server vrati pristupni token klijentske aplikacije moraju uključiti token unutar Authorization zaglavlja. Primjer jednog takvog zahtjeva:

curl -X POST -H "Authorization: Bearer PRISTUPNI_TOKEN""https://localhost:8443/person/1" 

Zaključak

Spring Security kao razvojni okvir najveću primjenu ima kao autorizacijski i autentifikacijski alat. Kao okvir uspješno rješava sljedeće OWASP TOP 10 problem:

  1. A2 - Broken Authentication and Session Management - pružajući niz mehanizama za autentifikaciju i upravljanje sesijama
  2. A4 - Insecure Direct Object References - pružajući mehanizme za autorizaciju unutar aplikacija
  3. A6 - Sensitive Data Exposure - Crypto modul pruža niz kriptografskih mogućnosti kako bi zaštiti osjetljive podatke
  4. A7 - Missing Function Level Access Control - pomoću autorizacijskih mehanizama unutar aplikacije
  5. A8 - Cross-Site Request Forgery (CSRF) - pružajući podršku za generiranje i validaciju CSRF tokena

Također Spring Security ukoliko se koristi JDBC način rješava i problem SQL injection-a. No međutim ukoliko koristimo neki ORM moramo se pobrinuti da se na toj razini osiguramo od SQL injection vrste napada. Kada govorimo o SQL injectionu u Java svijetu najviše se grešaka događa prilikom korištenja HQL (Hibernate Query Language). Većinom ljudi misle da će se Hibernate u tom slučaju pobrinuti za sve. No HQL je samo upitni jezik te također moramo primijeniti najbolje prakse za izbjegavanje SQL injection napada. Spring razvojni okvir prilikom svojih implementacija JPA repozitorija rješava problem SQL injection, no međutim ukoliko sami odlučimo pisati HQL upite ili SQL upite moramo obavezno paziti. Vidimo da ostalih 5 OWASP problema Spring Security ne rješava, no sama svrha Spring Security-a nije rješavanje svih sigurnosnih probleme nego da služi kao glavna komponenta za izgradnju autorizacijskih i autentifikacijskih sustava. Što se tiče same autorizacije i autentifikacije ovaj okvir pruža niz mogućnosti te gotovo postao standard u Java EE svijetu.

Literatura

  1. Spring Security Reference
  2. Spring Security
  3. OAuth2
  4. An Introduction to OAuth 2
  5. OAuth 2 Developers Guide
  6. Securing Your Tomcat App with SSL and Spring Security
Osobni alati
Imenski prostori
Inačice
Radnje
Orijentacija
Traka s alatima