Tag: GUID

UUID-GUUID zamiast domyślnego ID w Ruby on Rails

Wstęp

W pewnym projekcie nad którym ostatnio pracuję, postanowiłem skorzystać z UUIDów (co to są UUIDy dowiesz się tutaj) zamiast wykorzystywać domyślne ID'ki. Realizacja tego zadania w Railsach wymaga zastosowania pewnych niewielkich zabiegów, jednak całość jest całkiem prosta.

Generowanie UUIDów

Do generowania UUIDów wykorzystamy gem uuidtools:

gem 'uuidtools'

Generowanie odbywa się z pomocą poniższej metody:

UUIDTools::UUID.random_create.to_s

Migracje

Aby wykorzystać UUIDy, zamiast standardowego pola ID skorzystamy z pola (a jakże) UUID. Aby tego dokonac, wyłączymy całkowicie generowanie IDków dla migracji. Stworzymy w tym celu klasę bazową dla naszych migracji (można oczywiście deklarować to w każdej migracji z osobna ale komu się chce). Tworzymy w katalogu db/ plik uuid_migration.rb zawierający jedną metodę (migracja ta jest migracją bazową więc nie ma up, down czy też (w R3.1) change:

class UuidMigration < ActiveRecord::Migration
  def create_uuid_table(name, options = {})
    create_table name, options.merge(:id => false) do |t|
      t.string :uuid, :limit => 36, :primary => true
      yield t
      t.timestamps
    end

    add_index name, :uuid, 'UNIQUE'
  end
end

Od teraz możemy wykorzystać naszą migrację edytując migracje danych modeli, np:

require File.expand_path(File.dirname(__FILE__))+'/../uuid_migration.rb'

class CreateProducts < UuidMigration
  def change
    create_uuid_table :products do |t|
      t.string   :name,  :null => false
      t.float    :price, :null => false
      t.timestamps
    end
  end
end

Taka migracja spowoduje, że oprócz pól zadeklarowanych powyżej, powstanie także (automatycznie) pole uuid oraz timestampy. Tworząc dużo modeli opartych na UUIDach, taka metoda jest wygodniejsza (nie zapomnimy np. przez przypadek wyłączyć automatycznie tworzonego pola id).

Klasa bazowa dla naszych modeli

Zanim zaczniemy, chciałbym zaznaczyć, że wykorzystamy klasę bazową zamiast modułu tylko dlatego, że w systemie nad którym pracuję, występuje takie rozwiązanie (powody pozostawiam dla siebie ;) ). Niemniej jednak, nic nie stoi na przeszkodzie aby przerobić rozwiązanie zaprezentowane poniżej wersję modułową.

W gruncie rzeczy cały "myk" sprowadza się do informowania klas o tym że zamiast id używamy uuida oraz do jego inicjowania. Inicjować uuida będziemy zaraz przed zapisem obiektu do bazy (before_create):

require 'uuidtools'

class UuidRecord::Base < ActiveRecord::Base

  self.abstract_class = true
  self.primary_key = :uuid

  before_create :generate_uuid

  private

  def generate_uuid
    self.uuid = UUIDTools::UUID.random_create.to_s
  end

end

Tworzymy klasę

Po utworzeniu naszej migracji oraz innych potrzebnych fragmentów kodu, możemy przystąpić do utworzenia naszej pierwszej klasy:

class Product < UuidRecord::Base

  set_table_name  :products
  set_primary_key :uuid

end

Wadą "uuidowania" jest to, że musimy deklarować nazwę tabeli (jeśli dziedziczymy, jako moduł includowany już nie), oraz że w każdym modelu (mimo że ustawiliśmy w nadrzędnym), musimy deklarować, że uuid jest naszym kluczem głównym:

set_table_name  :products
set_primary_key :uuid

Relacje pomiędzy obiektami

Opisując wykorzystanie UUIDów, warto wspomnieć także o tym, jak przebiega stosowanie ich w relacjach has, belongs_to, itd. Ogólnie rzecz ujmując, to przebiega... tak samo. Ale... należy pamiętać o tym, że nie mamy pola ID! Dlatego deklarując relacje, musimy podawać klucz obcy oraz klucz główny:

  belongs_to :product,
    :primary_key => :uuid,
    :foreign_key => :product_uuid

  has_many :ordereds,
    :primary_key => :uuid,
    :foreign_key => :order_uuid

Tyle jeśli chodzi o UUIDy. Ich wykorzystanie nie jest trudne, pod warunkiem, że będziemy pamiętać o tym co napisałem powyżej.

Rails – losowe wartości klucza głównego – czyli dlaczego autoincrement to zło

MySQLowy autoincrement jest bez wątpienia potężnym i wygodnym mechanizmem. Jest też bardzo popularny. I trudno się dziwić ;) ustaw - zapomnij. Wygodne prawda? Jednak takie podejście niesie za sobą pewne nieciekawe konsekwencje.

Korzystając z interfejsu RESTowego jesteśmy zmuszeni (aczkolwiek raczej nikomu to nie przeszkadza ;) ) do używania identyfikatorów na zasadzie: artykuly/1234/komentarze/2156 gdzie mamy id artykułu i id komentarza.

Ale co w tym złego?

Niby nic - liczby jak każde inne. Jednak tutaj zaczyna się problem, ponieważ liczby te po pierwsze mogą nam powiedzieć dość dużo same z siebie - kiedy ustawione są na autoincrement numerowane są w kolejności powstania. Dzięki temu ktoś uważny jest w stanie ocenić jak dużo mamy komentarzy/logów/wpisów/zdjęć w systemie, nawet jeśli część nie jest dla niego dostępna.

Ciekawostka: Takiej metody użyli alianci żeby ocenić poziom produkcji czołgów niemieckich. Numery seryjne czołgów były właśnie inkrementowane.

Kolejną rzeczą jest bezpieczeństwo. Oczywiście stosujemy white listy i inne metody weryfikacji uprawnień użytkowników. Stosujemy też testy żeby sprawdzić czy wszystko jest należycie zabezpieczone. Niemniej jednak nie sposób uchronić się od wszystkich błędów.

Sprytny użytkownik

Załóżmy że mamy logi systemowe - część logów może przeglądać każdy użytkownik (te które dotyczą właśnie jego) a część tylko admin. No i implementujemy sobie podgląd danego logu w akcji show, gdzie w URLu podajemy ID.

Oczywistym rozwiązaniem byłoby zastosowanie filtra, który sprawdzałby uprawnienia i ew. odnotował że ten i ten user stara się uzyskać dostęp do zasobów chronionych. Jednak przez nieuwagę zapomnieliśmy to zrobić.

Co robi sprytny użytkownik?

Widząc że jego wpisy idą w miarę po kolei (np. 1, 5, 9,21, 23, ...) modyfikuje ręcznie URL i wchodzi na log o ID 2, 3, itd przeglądając rzeczy które teoretycznie powinny być dostępne tylko dla admina.

Inny sprytny pomysł

Dawno, dawno temu zamknięto serwis napisy.org i w polskim internecie zapanował strach wśród fanów anime że zamkną i świetny serwis animesub.info. Nie będę ukrywał że i ja się bałem ;) a że nie lubię siedzieć i czekać postanowiłem coś z tym zrobić. Za tamtych czasów serwis ten nie miał praktycznie żadnych zabezpieczeń, a pliki ściągać można było będąc niezalogowanym. Stosowano tam właśnie autoincrement dla plików. W bazie było ID pliku które jednocześnie było jego nazwą na dysku oraz był wpis w bazie do jakiego anime i odcinka jest ten plik.

Wystarczyło więc (wtedy jeszcze w delphi) napisać prosty program który będzie próbował pobrać wszystkie pliki od 0 do ostatniego (najnowszego). Metoda okazała się bardzo skuteczna. Aby nie wzbudzać podejrzeń pliki pobierane były z losowym delayem kilkusekundowym. Po dobie lokalna kopia ich bazy napisów była u mnie. Napisy usunąłem jak tylko potwierdzono że animesub zostaje :)

Na pocieszenie dodam że adminów animesub powiadomiłem i w miarę szybko zastosowali odpowiednie zabezpieczenia które skutecznie zabezpieczają obecnie ten system przed tego typu atakami.

Co zatem zrobić?

Osobiście znam dwa rozwiązania. Pierwszym z nich jest stosowanie GUIDów (Globally Unique Identifier) które wyglądają mniej więcej tak: 25892e17-80f6-415f-9c65-7395632f0223.

Jest to losowy ciąg cyfer oraz liter który generowany jest z jakiegoś skomplikowanego wzorca. W takim przypadku nie ma możliwości "jechania" po kolejnych wartościach bo jest ich po prostu za dużo.

Innym rozwiązaniem (które i ja stosuję troszkę częściej niż GUIDy) jest zrandomizowanie klucza głównego.

Implementacja tego rozwiązania jest bardzo prosta niezależnie od wybranego języka. Po prostu zamiast polegać na autoincremencie - losujemy ID z dużego zakresu (np. z miliona), sprawdzamy czy taki wpis już istnieje (na wszelki wypadek) i jeśli nie to dodajemy wpis. Jeśli tak to losujemy, sprawdzamy, dodajemy.

A jak to zrobić w Ruby on Rails?

A bardzo prosto. Pokusiłem się i napisałem nawet plugin który sam w sobie składa się z kilkunastu linijek, jednak według mnie jest bardzo wygodny. Nazwałem go acts_as_random co chyba oddaje w miarę sensownie jego funkcję.

Kod pluginu na licencji MIT (czyli rób z tym co chcesz ale jak coś nie będzie działać to też się nie czepiaj) dostępny do pobrania na końcu artykułu.

Jego użycie jest bardzo proste. Rozpakowujemy go do katalogu /vendor/plugins naszej aplikacji i od teraz możemy używać go w każdym modelu dziedziczącym po ActiveRecord.

Przykład:

class CoolClass < ActiveRecord::Base
    acts_as_random
end

Do naszego modelu dopisujemy tylko acts_as_random i już cała magia działa :) nic więcej nie trzeba robić a ID są generowane losowo, co można sprawdzić zapisując obiekt i wyświetlając jego id.

Oczywiście rozwiązanie moje nie jest wolne od wad. Przede wszystkim przy każdym zapisaniu obiektu następuje odpytanie czy obiekt o takim ID już nie istnieje (czyli +1 zapytanie do każdego save). Jeśli mamy system który generuje bardzo dużo danych warto pomyśleć nad GUIDami.

Kolejnym minusem jest to że jeśli będziemy mieli przykładowo 100 000 wpisów w danej tabeli a losujemy liczbę z przedziało 0..1 000 000 to mamy 10% szans że wylosujemy taką która już jest. Sprawia to że proces losowania musi być ponowiony i musi nastąpić kolejna weryfikacja ID (już +2 w tym wypadku do save'a).

W większości wypadków jednak losowanie z 1 000 000 w zupełności starcza a jeśli nie to zawsze można poprawić mój kod i zamiast z 1 000 000 losować z większego zakresu.

Rozwiązanie to jest wygodne (jedna linijka w modelu) i działa w większości przypadków - nie przeszkadzając przy tym w aplikacji (no chyba że ktoś korzysta z order by id).

Nie chroni ono w 100% jednak znacząco utrudnia życie potencjalnym "użyszkodnikom". Bo mając wpis o ID = 142145 następny może mieć wartość 235652, przez co szansa "trafienia" drastycznie spada w porównaniu z autoincrement.

Stosując to rozwiązanie oraz weryfikację uprawnień raczej nie powinno być problemów.

Jeśli więc tworzysz obiekty dużo rzadziej niż je wyświetlasz to to rozwiązanie jest dla Ciebie :)

Kod pluginu: acts_as_random

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑