UUID resource'a w REST zamiast ID z bazy a wydajność w PostgreSQL

0

Piszę swoją aplikację, która wykorzystuje architekturę mikroserwisów, więc każdy mikroserwis odpowiada subdomenie według DDD, oraz każdy ma swoje własne REST API.

W kilku mikroserwisach jako bazę danych wykorzystuję PostgreSQL, natomiast aplikacja korzysta ze Spring Data JPA/REST oraz HATEOAS.
Po przeszukaniu Stackoverflow i nie tylko, doszedłem do wniosku, że nie chcę według zasad wystawiać ID rekordu/zasobu na zewnątrz i zwracać go przez REST API, z powodu security.

Chciałbym natomiast wykorzystać i zwracać UUID, które byłoby przechowywane jako kolumna obok ID, i w REST API klient wysyłałby requesta wykorzystując UUID resource'a, a nie ID, na przykład tak: DELETE /users/91c60688-1779-483d-aaf1-b1e2dd02dab4 zamiast: /users/2343

PostgreSQL obsługuje od wersji 9.x typ UUID, który ma 16B i pozwala na tworzenie indeksów, więc mógłbym zrezygnować w ogóle z ID i korzystać z samego UUID, ale nie jestem pewien co do wydajnośći tego typu danych.

Które podejście polecalibyście jako nalepsze i nabezpieczeniejsze z puktu widzenia performance i bezpieczeństwa?

  1. Zwracać zwyczajnie ID z bazy danych i potem w REST API, i nie korzystać z UUID, co nie jest zalecane
  2. Dodać UUID jako dodatkową kolumnę i stworzyć logikę mapującą UUID <-> ID z metodami w Spring Data np. UserRepository: findById i findByUuid, co będzie wymagało dodatkowego SELECTa np. przy logice wykonującej PUT i DELETE
  3. Usunąć ID i korzystać wyłącznie z UUID z typem uuid w PostgreSQL, który zajmuje 16B (Podobno performance jest bardzo dobry?)
  4. Zamiast z UUID korzystać, użyć algorytmu hashującego i stworzyć hash ID i jego zwracać w REST API, żeby ukryć ID z bazy

Dzięki :)

0

Dodać UUID jako dodatkową kolumnę i stworzyć logikę mapującą UUID <-> ID

Po co trzymać dwa rodzaje idków?
To jest proszenie się o problemy z mapowaniem i całkowicie niepotrzebne komplikowane architektury już na starcie.

Usunąć ID i korzystać wyłącznie z UUID z typem uuid w PostgreSQL, który zajmuje 16B (Podobno performance jest bardzo dobry?)

Przeciętny PC ma obecnie między 8 a 16 GB RAMu oraz i5 na pokładzie, a Ty pytasz czy 16b to nie jest przypadkiem za dużo ze względu na wydajność? ;-p

Mógłbyś mieć indeks o wielkości kilku kilobajtów i prawdopodobnie nie zauważyłbyś różnicy, póki z Twojej aplikacji nie korzystałyby dziesiątki tysięcy użytkowników w jednej chwili.

Zamiast z UUID korzystać, użyć algorytmu hashującego i stworzyć hash ID i jego zwracać w REST API, żeby ukryć ID z bazy

  1. security by obscurity (prawie) nigdy nie jest dobrym rozwiązaniem.
  2. Wymyślanie własnych rozwiązań związanych z bezpieczeństwem (prawie) nigdy nie jest dobrym rozwiązaniem,
  3. Obliczanie hashy liczb mija się z celem, ponieważ bardzo łatwo jest po tej przestrzeni przeiterować, by odkryć pierwotną liczbę (cyfr jest wszak znacznie mniej niż np. drukowalnych znaków ASCII ;-)).

tl;dr bierz UUID, pamiętając jednak, że UUID nie jest zabezpieczeniem - w dalszym ciągu odpowiednie zasoby powinny mieć zaimplementowane uwierzytelnianie, ACLki i tak dalej.

0
  • W przypadku bazy zarządzanie spójnością PK-FK w oparciu o typy numeryczne będzie wydajniejsze. Typy liczbowe zajmują sporo mniej miejsca niż te 128 bitowe UUIDy. https://www.postgresql.org/docs/11/datatype-numeric.html
  • Partycjonowanie po UUIDach może działać tak sobie. Wybierając UUID na starcie wykluczasz list partitioning i range partitioning.
  • Enumeracja zasobów via UUIDa będzie trudniejsza dla atakującego (np. /moje/dziurawe/api/<id> vs /moje/dziurawe/api/<uuid>) - po <id> leci sekwencyjnie
  • W przypadku danych hierarchicznych (parent_id, id) nie widzę jakiejś szczególnej przewagi <id> vs <uuid>, tak by parent i child lądowały w jednym shardzie albo jednej partycji.
  • Nie rąbnij się i tego UUIDa nie generuj czasem jako tekstu, bo wyjdzie 36 znakowa katastrofa:)

Jaki problem chcesz rozwiązać?

0

Dzięki za odpowiedzi ;)

security by obscurity (prawie) nigdy nie jest dobrym rozwiązaniem.
Wymyślanie własnych rozwiązań związanych z bezpieczeństwem (prawie) nigdy nie jest dobrym rozwiązaniem,
Obliczanie hashy liczb mija się z celem, ponieważ bardzo łatwo jest po tej przestrzeni przeiterować, by odkryć pierwotną liczbę (cyfr jest wszak znacznie mniej niż np. drukowalnych znaków ASCII ;-))

Dokładnie jak mówisz, dlatego podałem możliwe rozwiązania problemu :D

@yarel

Dzięki za artykuł :). Chcę stworzyć proste REST API, w którym wystawiam REST resource, ale używam UUID na zewnątrz oraz ukrywam ID zasobów, żeby atakujący nie ich zgadł, na przykład tak dla testu:

Chcę po prostu korzystać z ID typu numerycznego SEQUENCE wewnątrz aplikacji, ale wystawiać już UUID na zewnątrz dla klientów, tak żeby zamaskować,i szukam na to fajnego rozwiązania, bez konieczności pisania dodatkowego zapytania jak to ma miejsce teraz, żeby znaleźć najpierw ID, muszę najpierw zrobić SELECT z UUID np. w UserFacade -> delete() albo update() gdybym chciał aktualizować rekord.

Przykładowo (Wiem, nakombinowane jest, dlatego chcę to wyprostować :) )

UserController

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
class UserController {

    private final UserFacade userFacade;

    @GetMapping(value = "/{uuid}")
    ResponseEntity<Resource<UserDto>> findOne(@PathVariable("uuid") String uuid) {
        Link link = linkTo(UserController.class).slash(uuid).withSelfRel();
        UserDto user = userFacade.show(uuid);
        Resource<UserDto> userResource = new Resource<>(user, link);
        return ResponseEntity.ok(userResource);
    }

    @DeleteMapping(value = "/{uuid}")
    ResponseEntity<UserDto> delete(@PathVariable("uuid") String uuid) {
        UserDto user = userFacade.delete(uuid);
        return ResponseEntity.ok(user);
    }
}

UserFacade (Korzystam z architektury hexagonalnej/ports&adapters i CQRS, stąd fasada)

@Transactional
@RequiredArgsConstructor
@Component
public class UserFacade {

    private final UserRepository userRepository;
    private final UserCreator userCreator;

    public UserDto add(UserDto userDto) {
        Objects.requireNonNull(userDto);
        User user = userCreator.from(userDto);
        user = userRepository.save(user);
        return user.dto();
    }

    public UserDto show(String uuid) {
        Objects.requireNonNull(uuid);
        User user = userRepository.findOneOrThrow(UUID.fromString(uuid));
        return user.dto();
    }

    public Page<UserDto> findAll(Pageable pageable) {
        Objects.requireNonNull(pageable);
        return userRepository.findAll(pageable).map(User::dto);
    }

    public UserDto delete(String uuid) {
        Objects.requireNonNull(uuid);
        User user = userRepository.findByUuid(UUID.fromString(uuid));
        User deletedUser = userRepository.deleteById(user.getId());
        return deletedUser.dto();
    }
}

UserRepository

interface UserRepository extends Repository<User, Integer> {
    User save(User user);
    User findById(int id);
    User findByUuid(UUID uuid);
    Page<User> findAll(Pageable pageable);
    User deleteById(int id);
    User deleteByUuid(UUID id);

    default User findOneOrThrow(UUID uuid) {
        User user = findByUuid(uuid);
        if (user == null) {
            throw new UserNotFoundException("Cannot find " + uuid);
        }

        return user;
    }

    default User findOneOrThrow(int id) {
        User user = findById(id);
        if (user == null) {
            throw new UserNotFoundException("Cannot find " + id);
        }

        return user;
    }
}

User

@Entity
@Getter
@Setter
class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Type(type="pg-uuid")
    private UUID uuid;
    
    // ...
}
0

Czemu default User findOneOrThrow(UUID uuid) zamiast default Optional<User> find(UUID uuid)? :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1