BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











- Kto mówi? - A kto dzwoni!?

09-06-2004 03:00 | emo3718
Identyfikacja obiektu wywołującego metodę w .NET

Szybki start

Zasada ukrywania danych i implementacji jest od dawna stosowana w językach obiektowych – C# nie jest tu wyjątkiem. Dzięki stosowaniu się do niej kod staje się łatwiejszy do ponownego użycia, zależności między klasami łatwiejsze do opanowania, a przez to życie programisty przyjemniejsze :). C#, podobnie jak inne tego typu języki definiuje kilka poziomów widoczności – od private do public – dzięki którym możemy ograniczać liczbę klas uprawnionych do operowania na danych polach, klasach czy metodach. Czasem te zdefiniowane poziomy przestają jednak wystarczać, np. w sytuacji, gdy metoda z jakiś względów musi być publiczna, ale chcemy, żeby była wywoływana tylko z jakiegoś miejsca aplikacji. Wtedy najczęściej nasze intencje umieszczamy w komentarzach i liczymy, że programiści-klienci naszej klasy będą te komentarze respektować. Nie możemy wymóc na kompilatorze, żeby odmawiał kompilacji wywołań publicznej metody przez nieprzeznaczone do tego klasy.

Ale, parafrazując znane przysłowie: „gdzie kompilator nie może, tam runtime’a pośle” wszystkie tego typu sprawdzenia możemy wykonać sobie sami. Jak? Odpowiedzią na to pytanie jest klasa StackTrace z przestrzeni nazw System.Diagnostics. Każdy Czytelnik dobrze zna tę klasę, więc dla formalności tylko przypomnę, że mamy z nią najczęściej do czynienia przy wystąpieniach wyjątków - z jej pomocą generowane są raporty o miejscu wystąpienia wyjątku w programie, które wyglądają mniej więcej tak:

Unhandled Exception: System.Exception: Jakiś komunikat
  at DefaultNS.MyClass.b() in c:\StackToy\Main.cs:line 37
  at DefaultNS.MyClass.a() in c:\StackToy\Main.cs:line 36
  at DefaultNS.MyClass.Main(String[] args) in c:\StackToy\Main.cs:line 48

Ciekawą właściwością klasy StackTrace jest to, że można utworzyć jej obiekt w dowolnej chwili (nie trzeba generować wyjątku), a utworzona w ten sposób instancja zawierać będzie kompletny stos wywołań w danym momencie wykonywania programu. Ponadto, poza tekstową reprezentacją udostępnia ona wygodny, obiektowy interfejs do każdego poziomu wywołań (ramki stosu - frame).

Nazwę typu wywołującego naszą metodę uzyskamy w następujący sposób:

W metodzie, do której chcemy ograniczyć dostęp, tworzymy obiekt stosu wywołań:

Stack stack = new StackTrace();

Obiekt stosu oferuje listę ramek – wywołań metod, które przywiodły program do naszej metody. Ramka pierwsza od góry (z indeksem 0) to tu, gdzie utworzyliśmy obiekt StackTrace. Ramka następna to metoda obiektu-nadawcy. Uzyskujemy ją za pomocą wywołania:

StackFrame frame = stack.GetFrame(1);

W ramce z kolei znajduje się metoda obiektu-nadawcy. Można z niej wydobyć deklarujący ją typ, o który nam chodzi – a właściwie jego nazwę:

Type type = frame.GetMethod().DeclaringType;

Teraz już łatwo sprawdzimy, czy klientem naszej metody jest właściwy obiekt, prawda?

if (type == typeof(WlasciwyObiekt)) {
 // normalne zachowanie
} else {
 // wyrzucenie wyjątku / nie robienie niczego
}

Po takim zabiegu, choć niepoprawny program nadal się skompiluje, to nasza metoda nie zadziała, gdy jest wywoływana w niewłaściwy sposób. Prawda, że jest to rozwiązanie skuteczniejsze, niż poleganie na komentarzach? Jeśli na niewłaściwe wywołanie zareagujemy wyjątkiem, będzie jeszcze lepiej, bo programista używający naszej klasy w niewłaściwy sposób zostanie o tym natychmiast poinformowany.

Stosując wykrywanie nadawcy komunikatu odbieranego przez naszą metodę w czasie wykonania zaimplementowaliśmy sobie coś w rodzaju własnych zakresów widoczności. Sprawdzaliśmy teraz jednak tylko typ obiektu-nadawcy, ignorując poprzednie wywołania na stosie. Można sobie jednak wyobrazić sytuację, w której zależy nam na ścisłej kontroli całego ciągu wywołań – na przykład: mamy klasę konta bankowego Account, która daje sobą manipulować jedynie zaufanej klasie Manager. Jednak Manager nie sprawdza, kto korzysta z jego usług i może dojść do sytuacji, kiedy złośliwy klient zdobędzie skądś referencję do Manager’a i za jego pomocą będzie chciał operować na koncie. Jest to oczywiście nieuprawnione działanie, ale konto sprawdzi jedynie swojego bezpośredniego poprzednika w stosie wywołań i da się w ten sposób oszukać. Aby tego uniknąć konto powinno sprawdzić cały stos wywołań, upewniając się, że są na nim tylko zaufane typy.

Obydwie opisane sytuacje demonstruje dołączony do artykułu program. Aby go uruchomić należy załadować źródło (Main.cs) rozpakowane z pliku zip do ulubionego IDE, względnie umieścić je w jakimś katalogu, jeśli planuje się kompilować je ręcznie. Ważne jest, aby kompilować z przełącznikiem

/debug+

(ręcznie) lub w trybie debug wewnątrz IDE. Programy skompilowane w inny sposób mogą mieć podczas wykonywania inny stos wywołań – kompilator przy optymalizacji dokonuje różnych skrótów i może się okazać, że kilka metod, które powinny się znaleźć po drodze gdzieś wyparowało. Jest to niestety ograniczenie programów wykorzystujących stos wywołań do identyfikacji obiektów-nadawców, które trzeba brać pod uwagę projektując takie systemy. Skompilowany program uruchamiamy wywołując Main.exe.

W aplikacji zdefiniowane są klasy:

Account – klasa konta udostępniająca właściwość salda (Amount). Saldo daje się bez problemu odczytać, ale do jego modyfikacji uprawnione są bezpośrednio tylko klasy Manager i Bank.

Manager – klasa operująca na koncie, posiada jedyną metodę, służącą do zmiany salda danego konta.

EvilIntruder – klasa próbująca zmienić saldo danego konta identycznie jak Manager, jednak nie będąc do tego uprawnioną.

SmartEvilIntruder – klasa próbująca nielegalnie zmienić saldo konta wykorzystując do tego Manager’a, któremu normalnie konto ufa.

SmartAccount – ulepszona klasa konta, sprawdzająca cały łańcuch wywołań doprowadzających do operacji zmiany salda i odmawiająca wykonania metody, gdy choć jedna z klas na stosie nie jest zaufana.

Bank – główna klasa aplikacji, która zestawia ze sobą pozostałe klasy w różnych kombinacjach, aby przetestować wyniki interakcji. Po uruchomieniu, Bank realizuje następujące scenariusze:

  • wywołanie legalnej operacji na koncie

  • wywołanie nielegalnej operacji na koncie przez zwykłego intruza (EvilIntruder)

  • atak sprytnego intruza (SmartEvilIntruder) na zwykłe konto

  • atak sprytnego intruza (SmartEvilIntruder) na sprytne konto (SmartAccount)

Wyniki działania w każdym wypadku wypisywane są na konsolę. Jeśli nadawca został rozpoznany jako zaufana klasa, wypisywany jest komunikat „Nadawca OK”, w przeciwnym wypadku „Zły nadawca”. Klasa konta wypisuje dodatkowo nazwę nadawcy, a sprytnego konta zawartość stosu wywołań, na podstawie którego podjęła swoją decyzję.

Efekt uruchomienia programu Bank

Uogólnienie

Przedstawiony program pokazał, jak można odmówić dostępu nieuprawnionym klasom dzięki ich identyfikacji w czasie wykonania. Obiekt, którego metoda jest wywoływana (umówmy się „serwer”) tworzy obiekt stosu wywołań, uzyskuje z niego nazwę wywołującego typu („klienta”) i zezwala, bądź odmawia wykonania danej usługi. Chcąc uogólnić, można powiedzieć, że gdy klient żąda od serwera usługi, ten zasięga informacji na stosie wywołań, używa tej informacji do podjęcia decyzji, w jaki sposób obsłużyć klienta i następnie go obsługuje – jak na diagramie sekwencji poniżej:

Serwer obsługuje klienta wykorzystując informacje ze stosu wywołań

Na diagramie krok uzyskania informacji ze stosu nazwałem „kto mnie woła”, żeby uniknąć nadmiernego abstrahowania, co mogłoby zaciemnić ogólny obraz. Ten krok może z powodzeniem zostać zastąpiony przez uzyskanie jakiejkolwiek informacji możliwej do uzyskania ze stosu. Tak samo decyzja, co robić, nie musi ograniczać się jedynie do wykonania bądź nie jakiejś operacji. Zbadajmy, jakie pożyteczne informacje dadzą się wyciągnąć ze stosu.

Co w stosie piszczy

Obiekt StackTrace oferuje właściwość FrameCount, dzięki której możemy badać, jak głęboko w stosie wywołań już się znajdujemy. Być może nie w najbardziej elegancki sposób, ale skutecznie pozwala nam to np. na wychodzenie z podejrzanie głębokiej rekursji. Właściwość ta jest też użyteczna przy iterowaniu po ramkach stosu, bo niestety StackTrace nie udostępnia żadnej kolekcji, po której możnaby przespacerować się ulubioną przez nas, programistów C# konstrukcją foreach. Obiekt stosu wywołań oferuje też dostęp do nazw plików z kodem źródłowym i numerów linii, w których następowały wywołania (trzeba użyć konstruktora z parametrem bool), ale nie zalecam nikomu nadużywania tych informacji, by nie popaść w skrajność. Ciekawe informacje zaczynają się od wywołania metody GetFrame, której podajemy numer interesującej nas ramki stosu, a która zwraca nam tę ramkę w postaci obiektu StackFrame. Tenże obiekt zawiera opcjonalną informację o pliku z kodem źródłowym, wierszu i kolumnie, w których nastąpiło wywołanie, ale przede wszystkim obiekt typu MethodBase (zwracany przez GetMethod), przez który dobierzemy się do „mięska”. Klasa MethodBase stanowi typ bazowy dla klas konstruktorów i metod. Z jej najciekawszych właściwości warto wymienić:

  • Attributes – atrybuty w stylu widoczności metody, czy jest abstrakcyjna, statyczna, wirtualna itp. – jest typu MethodAttributes (do większości z tych atrybutów można się dostać przez inne, łatwiejsze w użyciu właściwości MethodBase). Dzięki tej właściwości możemy np. ograniczyć dostęp do naszych metod jedy

  • DeclaringType – typ deklarujący metodę – kolejne źródło informacji o wywołującym obiekcie. Klasy Type

  • ReturnType (tylko po zrzutowaniu do typu MethodInfo, jeśli właściwość IsConstructor ma wartość false) – typ zwracanej wartości

A jej ciekawe metody to:

  • GetCustomAttributes – dzięki której mamy dostęp do wszystkich atrybutów (podklas Attribute) przypisanych danej metodzie

  • GetParameters – pozwalającą sprawdzić, jakie parametry formalne przyjmuje metoda

  • IsDefined – sprawdzająca, czy atrybut danej klasy jest dla tej metody zdefiniowany

Jeśli jednak porównać całość dostępnych informacji o obiekcie wywołującym metodę do góry lodowej, to MethodBase jest zaledwie jej czubkiem, a dolną częścią tej góry jest dopiero obiekt klas Type uzyskany przez właściwość DeclaringType. Tutaj dopiero możemy pobuszować sobie we właściwościach i metodach – jest ich zbyt wiele, aby zanudzać nimi Szanownego Czytelnika. Wystarczy wspomnieć, że mamy dostęp do listy pól, metod, implementowanych interfejsów, atrybutów, typów zagnieżdżonych, przestrzeni nazw i wielu, wielu więcej danych.

Powiązania między obiektami uzyskanymi ze stosu wywołań – niewielki fragment. Etykiety strzałek oznaczają nazwę właściwości, lub metody, za pomocą której uzyskuje się pożądany obiekt.

Dzięki posiadaniu tylu informacji o obiektach-nadawcach możemy bardzo precyzyjnie dzielić je na grupy i dostosowywać zachowanie naszej metody do każdej z nich z osobna. Nie ma problemu żeby np. umożliwić wywołanie naszej metody jedynie obiektom z przestrzeni nazw „My.Space”, implementującym interfejs „MyInterface” i posiadającym na dodatek wirtualną metodę „MyMethod” zwracającą typ „MyType”. Możliwości są tu bardzo duże, dlatego też warto mieć na uwadze znaną i cenioną zasadę KISS (dla niewtajemniczonych: Keep It Simple Stupid), żeby być w stanie zapanować nad rozwijaną aplikacją.

Obsługujemy klienta

Mając nadawców posegregowanych przyszła pora na podejmowanie decyzji, czym powinno się różnić wywołanie naszej metody dla każdej z tych grup. W przykładzie bankowym, konto po prostu nic nie robiło, gdy typ nadawcy nie należał do zaufanych. Rozważyliśmy przy tym możliwość wyrzucenia wyjątku, który szybko uświadomiłby programiście istniejący błąd. W systemie bankowym możnaby było dodatkowo zarejestrować zdarzenie nieuprawnionego dostępu do jakiegoś logu – trudno wymyślić inne sensowne zachowania w tym przypadku. To, jakie zachowanie metody jest sens różnicować, zależy już mocno od typu aplikacji. Niezależnie jednak od tego zmiana zachowania będzie ulokowana w jednym, kilku, lub wszystkich z następujących obszarów:

  • efekty uboczne (tu mamy najszersze pole do popisu)

  • zwracana wartość (i jej typ, który może być podtypem deklarowanego typu zwracanej wartości)

  • parametry wyjściowe (podobnie do zwracanej wartości)

  • rodzaj wyrzucanych wyjątków

Pod względem efektów ubocznych działania metody trudno spekulować, jakie to efekty mogłyby być, bo jest to ściśle zależne od aplikacji, ale na pewno nie można uznać za dobrą praktykę znaczące odchodzenie od specyfikacji danej metody.

Zwracana wartość jest ciekawym obiektem do zmian, bo wpływa bezpośrednio na obiekt wywołujący naszą metodę. Można np. wyobrazić sobie sytuację, że klasa przechowująca listę produktów w sklepie zwraca tę listę różnie uporządkowaną dla różnych kontrolek. Interesujące eksperymenty można przeprowadzać także podmieniając typ zwracanej wartości – oczywiście musi być to podtyp zadeklarowanego typu zwracanej wartości. Wtedy klient, nieświadomy podmiany może wywołać wirtualną metodę zwróconego obiektu, co spowoduje wykonanie kodu z podmienionej klasy. W innej sytuacji, klient może być świadomy tego, że otrzymuje podmieniony obiekt i wykorzystywać tą wiedzę do manipulacji na tym obiekcie – np. gdy typ zwracanej wartości posiada tylko jedną metodę, a jego podtyp pięć, klient rzutuje tę wartość do podtypu i może korzystać z tych pięciu metod, czego nie byłby w stanie uczynić z samym typem bazowym. Ta sytuacja obrazuje pewien kontrakt między obiektem wywołującym, a obiektem, którego metoda jest wywoływana: „należysz do moich znajomych, dlatego zwrócę ci obiekt podtypu, z którym będziesz wiedział, co zrobić”.

Parametry wyjściowe nie różnią się znacząco od zwracanej wartości, dlatego je pominę, a wspomnę o wyrzucanych wyjątkach. Te obiekty nie mają ograniczeń co do typu, jak zwracane wartości. Jeśli zdecydujemy się na wyrzucanie wyjątków o typie dziedziczącym ze zwykle wyrzucanego typu, dotychczasowi klienci klasy mogą w ogóle nie odczuć zmian – ich klauzule catch wyłapią też nasze wyjątki. Jednak klienci świadomi tej zmiany mogą obsłużyć nasze nowe wyjątki, jeśli chcą. Co innego dzieje się, gdy zdecydujemy się na wyrzucanie wyjątków o typie zupełnie nieoczekiwanym przez dotychczasowych klientów. Możemy w ten sposób narobić sporego bałaganu, dlatego zawsze musimy przewidzieć skutki planowanych zmian.

Możliwe zastosowania

Opisany przykład ograniczania dostępu dla uprawnionych klas możnaby rozwinąć. Jego wadą jest zakodowanie na sztywno nazw uprawnionych klas – mogłyby być one trzymane w pliku. W tym miejscu możnaby pokusić się o dalsze uogólnienie w celu zbudowania uniwersalnego środowiska sterowania widocznością składników aplikacji – to, jakie składniki miałyby być widoczne dla jakich, definiowałoby się w zewnętrznym pliku, a każda metoda sprawdzałaby, czy jest legalnie wywoływana. Oczywiście pisanie takich sprawdzeń w każdej metodzie byłoby bardzo pracochłonne – nie polecam robić tego ręcznie, a jedynie za pomocą programowania aspektowego, opisanego w innym artykule na CodeGuru.

Informacji o stosie możemy użyć także do innych celów. Możemy je np. wykorzystać przy modyfikacji systemu, gdy posiadamy metodę, do której odwołuje się wiele klas, a uznamy, że część z nich powinna odwoływać się nie do niej, a do innej naszej metody (np. metoda używana jest do rejestrowania zdarzeń w logu, a chcemy, żeby zdarzenia z kilku wybranych klas wypisywane były bezpośrednio na konsolę). Naturalnie moglibyśmy zmienić odwołania we wszystkich klasach, w których byłoby to konieczne, ale nie zawsze mamy taką możliwość, bo np. pracując w zespole programistycznym nie mamy praw do zmiany tych klas, a koledzy, którzy mogliby przeprowadzić te zmiany, są chwilowo niedostępni. Aby szybko przeprowadzić tę zmianę, wystarczy, że w naszej metodzie wykryjemy, czy wywołanie przychodzi od jednej z tych klas i jeśli tak, wywołać tą inną metodę.

Dane ze stosu mogą być alternatywą dla parametrów metody. Zamiast badać parametr typu bool, można sprawdzić na przykład, czy wywołujący obiekt posiada zdefiniowany odpowiedni atrybut, lub czy implementuje dany interfejs.

Posługiwanie się informacjami uzyskanymi poprzez stos wywołań jest niezastąpione w sytuacji, gdy metoda powinna być wywoływana jedynie przez określonych klientów. Używane czasem rozwiązanie, polegające na przekazywaniu obiektu nadawcy w parametrze nie spełnia swojej roli, bo referencję do obiektu uprawnionej klasy może zdobyć obiekt nieuprawniony, by następnie posłużyć się nią w celu oszukania metody. Co prawda ze stosu nie uzyskamy referencji do obiektu wywołującego naszą metodę, ale przynajmniej zagwarantujemy, że obiekt ten będzie legalnego typu.

Podsumowanie

Ograniczeniami w stosowaniu opisanego stylu programowania jest konieczność kompilowania systemów w trybie debug, co nie zawsze jest dopuszczalne. Podejście to może rodzić też problemy przy refaktoryzacji, jeśli posługujemy się nazwami typów w postaci łańcuchów tekstu – zmieniane nazwy typów należy pozmieniać wtedy również w tych łańcuchach. Można tego uniknąć, stosując jedynie literały typów. Problem ten jest typowy dla wszystkich zastosowań mechanizmu refleksji, z którego w tym artykule intensywnie korzystaliśmy.

Podane w artykule przykłady nie zamierzają wyczerpać wszystkich możliwości wykorzystania stosu wywołań - najwięcej pomysłów rodzi się przy pracy nad  konkretnymi aplikacjami. Mam nadzieję, że ich przytoczenie zainspiruje Czytelnika do własnych poszukiwań przy tworzeniu aplikacji dla .NET.

Dostosowanie działania metody do jej klientów pozwala szybko zmieniać zachowanie ich określonych grup, bez posiadania uprawnień do modyfikowania ich kodu źródłowego. Dzięki wielości informacji dostępnych ze stosu wywołań selekcję grupy klientów możemy przeprowadzać bardzo precyzyjnie. Możemy też obsługiwać klientów bez znajomości z góry ich nazw - przez definiowanie atrybutów, jakie muszą posiadać, lub interfejsów, jakie muszą implementować.

Ogólnie można powiedzieć, że dodając do bibliotek standardowych .NET klasę StackTrace Microsoft podarował programistom dużą elastyczność w rozwijaniu aplikacji.

 

Załączniki:

Podobne artykuły

Komentarze 9

droid7395
droid7395
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Autor przedstawia zupełnie nowy punkt widzenia na projektowanie aplikacji.
User 79209
User 79209
2 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Super pomysl, naprawde ciekawy. Co do zastosowan, to wydaje mi się ze tryb debug jest tu duzym ograniczeniem.
tom_london7396
tom_london7396
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Może i ciekawe podejście, ale w poważnych zastosowaniach nie do użycia: koniecznosc trybu debugowania jest nie do przyjecia w powaznych zastosowaniach. Stosy wywolan istnieja nie od dzisiaj, a nie stosuje sie ich do zapewnienia security, wiec traktuje to jedynie jako ciekawostke.
yossa7397
yossa7397
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Fajny pomysł, ale za dużo ograniczeń żeby go stosować w praktyce.
kruq_asap
kruq_asap
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Przydatny
User 131096
User 131096
189 pkt.
Junior
21-01-2010
oceń pozytywnie 0
ciekawe, aczkolwiek niepraktyczne
tkazmierski164
tkazmierski164
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Interesujący pomysł na zapewnienie bezpieczeństwa transakcji z poziomu jezyka programowania.
uzytkownik usuniety
uzytkownik usuniety
3556 pkt.
Guru
21-01-2010
oceń pozytywnie 0
Mimo że rozwiązanie nie jest praktyczne - jest oryginalne! Podobało się.
User 79467
User 79467
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
niezly pomysl, ale ze wzgledu na ograniczenia raczej tylko jako ciekawostka
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.