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 ahu.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"); } }
- hozzunk létre egy
- 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 azauditProvider
-nek megfelelőenString
- a
@EntityListeners(AuditingEntityListener.class)
is átkerül az absztrakt osztályra @MappedSuperclass
-val jelezzük, hogy bár ez így önmagában nem egyEntity
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. aContact
estén:1
public class Contact extends Auditable<String> { ... }
- az
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
JDBC Authentication¶
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
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ó: