Kihagyás

4. gyakorlat

Auditing

Spring Data JPA

  • field annotációk:
    • @CreatedDate
    • @CreatedBy
    • @LastModifiedDate
    • @LastModifiedBy
    • @EntityListeners(AuditingEntityListener.class)
    • ne felejtsük el a Created-eket megvédeni a null-ra update-eléstől (updatable = false)
  • a @CreatedBy és a @LastModifiedBy field-ekhez még kell egy kis konfigurálás
    • hozzunk létre egy JpaAuditConfig osztályt a hu.suaf.contact.config csomagban:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      package hu.suaf.contacts.config;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.data.domain.AuditorAware;
      import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
      
      import java.util.Optional;
      
      @Configuration
      @EnableJpaAuditing(auditorAwareRef = "auditProvider")
      public class JpaAuditConfig {
      
          @Bean
          public AuditorAware<String> auditProvider() {
              return () -> Optional.ofNullable("valaki");
          }
      
      }
      
  • az újrafelhasználhatóság jegyében szervezzük ki a Contacts osztály audit-hoz tartozó részeit egy absztrakt ősosztályba:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    package hu.suaf.contacts.model;
    
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.data.annotation.CreatedBy;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.Column;
    import javax.persistence.EntityListeners;
    import javax.persistence.MappedSuperclass;
    import java.util.Date;
    
    @Getter
    @Setter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class Auditable<U> {
    
        @Column(updatable = false)
        @CreatedDate
        private Date createdAt;
    
        @Column(updatable = false)
        @CreatedBy
        private U createdBy;
    
        @LastModifiedDate
        private Date lastModifiedAt;
    
        @LastModifiedBy
        private U lastModifiedBy;
    
    }
    
    • az U-t azért vezettük be, hogy később kevesebbet kelljen módosítsunk, egyelőre legyen az auditProvider-nek megfelelően String
    • a @EntityListeners(AuditingEntityListener.class) is átkerül az absztrakt osztályra
    • @MappedSuperclass-val jelezzük, hogy bár ez így önmagában nem egy Entity azaz nem szeretnénk külön táblában eltárolni, de a gyerek osztályok tábláiban jelenjen meg az itt szereplő 4 field
    • az auditálandó entitásainkat innentől kezdve származtassuk egyszerűen az Auditable-ből, pl. a Contact estén:
      1
      public class Contact extends Auditable<String> { ... }
      

Spring Security

Alapok

A Spring keretrendszer biztosít számunkra több megoldást is az alkalmazások biztonságossá tételéhez. Csapjunk is bele a közepébe. Ahhoz, hogy a Spring nyújtotta biztonsági lehetőségeket használhassunk a pom.xml-hez hozzá kell adnunk a következő függőséget:

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Ellenőrizzük az IntelliJ-ben, hogy a Maven fül alatt a dependencies lenyílóban szerepel-e a spring security. Amennyiben nem jelent meg, akkor nyomjunk meg a Reload All Maven Projects (kis újratöltős ikon) gombot, hogy ez megoldódjon. Bármennyire is hihetetlen, de mást nem kell tennünk, mint hogy elindítjuk az alkalmazásunkat. Ezután bármilyen URL-re navigálunk, akkor egy HTTP Basic Authentication dialógus ablakot kapunk ahol meg kell adni a felhasználónév és jelszó párost. A felhasználó ebben az esetben mindig user, míg a jelszót az alkalmazás indításakor a console-ra írja ki a rendszer valami hasonló formában: Using generated security password: a01b98a5-495b-4593-b9e4-7b6de9a6bbe3. A generált jelszó nyilván változik minden egyes futáskor, így az nem egyezik meg az itt megadottal (mindenki az általa megtalált egyedi jelszót használja). Miután megadjuk a felhasználónév és jelszó párost, akkor gond nélkül látnunk kell az alkalmazásunkat olyan formában amilyenben előtte is volt.

HTTP Basic Authentication (BA)

Az egyik legegyszerűbb authentikációs módszer, melynek folyamán a HTTP kliens egy felhasználónevet és egy jelszót továbbít a kéréssel együtt. Ezen adatokat a HTTP kérés fejlécében, Authorization: Basic <credentials> formában adja meg, ahol a credentials Base64-es encoding-al szerepel és tartalmazza a felhasználónevet és a jelszót (plain textben ezeket : választja el egymástól). Ehhez az authentikációs módhoz nem szükséges cookie-kat, session azonosítókat, login oldalt karbantartani, cserébe viszont elég gyér a biztonság amit kapunk (sima Base64-es kódoláűssal megy a jelszó, ami nincs titkosítva semmilyen módon). Az Authorization header-t minden kérésnél el kell küldeni, így a böngészők ezt valamennyi ideig cache-elik (böngészőként eltérő lehet), hogy ne kelljen minden egyes kérés előtt ezt bekérni.

A fenti alkalmazásban, mivel minden URL-t levédünk, így a szerver elsőkörben egy olyan választ ad, melynek fejlécében a HTTP 401 Unauthorized és a WWW-Authenticate elemek találhatóak meg. A kliens így fogja tudni, hogy BA-t kell használni és ezért a default 'login form'-ot használja ennek bekérésére, majd a fent leírt módon kódolja a bekért infokat és elküldi az Authorization headerrel együtt a kérést. Bővebb infoért lásd a következőt: RFC7617.

No, de visszatérve az alap Spring-es megvalósításra. A következőket kapjuk, ha csak behúzzuk a security függőséget és semmi egyebet nem teszünk:

  • minden kérést authentikálni kell
  • nincsenek szerepkörök
  • nincs saját login oldal
  • BA használata
  • egyetlen felhasználó (user)

A fentiek áthidalásához saját konfigurációt kell készítenünk.

Egyedi biztonsági konfiguráció

Hozzunk létre egy új osztályt (pl.: SecurityConfig néven)! Ahhoz, hogy a Spring konfigurációként kezelje ezt az osztály, szükséges, hogy ellássuk a @Configuration annotációval. Továbbá a security config-hoz kell az @EnableWebSecurity annotáció. Ezek mellett az osztályunkat a WebSecurityConfigurerAdapter osztályból kell származtatni.

1
2
3
4
5
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

A fenti konfig egyelőre nem csinál túl sok mindent, de már el tudjuk indítani az alkalmazásunkat (ugyanúgy működik mint eddig, ha minden jól megy).

Ahhoz, hogy egyedi konfigot használhassunk az ősosztály metódusait (configure nevűeket) tudjuk felüldefiniálni.

Saját magunk írjuk elő, hogy az alkalmazásban minden útvonalra csak bejelentkezett felhasználókat engedünk és HTTP Basic Auth-ot használunk

Ehhez a következőket kell tennünk:

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/*").authenticated()
            .and()
            .httpBasic();
}

A paraméterül kapott HttpSecurity objektumon kell egy hívást tennünk, mely segítségével az összes kérést ellenőrizni fogjuk: ez az authorizeRequests célja. A következő, hogy megadjuk, hogy mely útvonalakhoz milyen jogok kellenek. Ezt lehet az antMatchers hívásokkal elintézni, melyekkel útvonalat vagy útvonalakat adhatunk meg és utána egy újabb láncolt hívással megadhatjuk, hogy mit várunk el ezeken az útvonalakon (jelen esetben: authenticated, azaz legyen bejelentkezve), később szofisztikáltabb megadási módokat is bemutatunk. Utolsó lépésként megadjuk, hogy a HTTP Basic Auth-al kell végezni az autentikációt: ˛httpBasic. Az and hívásokkal mindig visszakapjuk a HttpSecurity objektumunkat, így lehet szépen dinamikusan láncolni a konfigurációs megadásokat.

Ezen a ponton álljunk meg és próbáljuk ki az alkalmazásunkat. Felhasználó továbbra is user, illetve a konzolra megint kapunk egy generált jelszót.

In Memory Authentication

Következő lépésként a felhasználók körét adjuk meg! A legegyszerűbb, hogy a felhasználókat in-memory adjuk meg. Ezt leginkább teszteléskor szokás használni. Ehhez egy másik konfigurációs metódust kell felüldefininálni, mégpedig valahogyan így:

1
2
3
4
5
6
7
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("user")
            .password("user123")
            .roles("USER");
}

Az alkalmazásunk el fog indulni, de még nem fog működni, mert valami még hiányzik, de előtte vegyük sorra, hogy a fentiek mit csinálnak.

1
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

Azaz elfelejtettük megadni, hogy hogyan legyenek encode-olva a jelszavak. Ezt a következő Bean hozzáadásával tudjuk megoldani:

1
2
3
4
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

Ekkor még nem fog működni a belépés, mivel a form-ból már az encode-olt jelszó megy tovább, a config-ban pedig még a nyers jelszó van jelenleg. Ezt javítsuk a következő módon:

1
2
3
4
5
6
7
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("user")
            .password(passwordEncoder().encode("user123"))
            .roles("USER");
}

JDBC Authentication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    public DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource);
    }

UserDetailsService

Készítsünk el egy az eddiginél jóval szofisztikáltabb authentikációs megközelítést.

Először hozzuk létre a User és Role modelleket:

  • ContactUser
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package hu.suaf.contacts.model;
    
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import javax.persistence.*;
    import javax.validation.constraints.Email;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    
    @Entity
    @Data
    @NoArgsConstructor
    public class ContactUser implements UserDetails {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        private String username;
        private String password;
        @Email
        private String email;
    
        @ManyToMany
        @JoinTable(
                joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
                inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
        )
        private Collection<Role> roles;
    
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return List.of(new SimpleGrantedAuthority("ROLE_USER"));
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
  • Role
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package hu.suaf.contacts.model;
    
    
    import lombok.Data;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    @Entity
    @Data
    public class Role {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        String name;
    
    }
    

További anyagok

A gyakorlat anyagáról készült videó:

SUAF_04_gyak


Utolsó frissítés: 2021-10-13 14:49:41
Back to top