poniedziałek, 7 czerwca 2010

Schemat uwierzytelniania z własnymi komunikatami błędu

Witam

Po dłuższej przerwie wracam z tematem który został poruszony na ostatnich szkoleniach. Temat ten nie został rozwiązany, dlatego dzisiaj postaram się dokładnie go opisać. 

Podczas tworzenia własnych schematów uwierzytelniania często spotykanym problemem jest brak możliwości przekazywania stosownej informacji użytkownikowi. Często zdarza się, że użytkownik chcący zalogować się do naszej aplikacji nie ma do niej dostępu z innych powodów niż podanie złego hasła lub identyfikatora. Wypadałoby zatem poinformować użytkownika o problemach z jego kontem - blokada, brak płatności itp itd.  Niestety podstawowa funkcjonalność ogranicza się do stanu zero-jedynkowego. Na szczęście istnieją metody na zmianę stanu rzeczy poprzez prostą edycję funkcji uwierzytelniania i procesu Login na stronie Logowania. 

Tyle tytułem wstępu. Teraz do rzeczy.

Zakładam, że aplikacja posiada własny schemat uwierzytelniania, który pracuje na funkcji custom_auth. Funkcję uwierzytelniającą umieszczono poniżej:  

create or replace
FUNCTION CUSTOM_AUTH (p_username in number, p_password in VARCHAR2)
return BOOLEAN
is
l_stored_password varchar2(4000);
l_count number;
begin
select count(*) into l_count
from users where upper(usr_name) = upper(p_username);

IF l_count > 0 then
select usr_password into l_stored_password
from users where upper(usr_name) = upper(p_username);
if p_password = l_stored_password then
return TRUE;
else
return FALSE;
end if;
ELSE
return FALSE;
END IF;

end;

Funkcja ta sprawdza czy dany użytkownik istnieje w tabeli users a następnie konfrontuje hasło w bazie danych z hasłem podanym podczas logowania. W zależności od tego czy hasło jest zgodne czy tez nie zostaje zwrócona wartość TRUE lub FALSE. Na podstawie tej wartości APEX przedstawi naszemu użytkownikowi informację zawartą w Text Message 'INVALID_CREDENTIALS'. Czyli coś w rodzaju "Nieprawidłowe hasło. Spróbuj ponownie." 

Wszystko fajnie ale co jeśli wprowadzimy możliwość blokowania konta. Jeśli użytkownik wpisze 3 razy nieprawidłowe hasło to jego konto zostanie zablokowane. Zmieniamy więc funkcję tak aby przybrała następujący kształt:

create or replace
FUNCTION CUSTOM_AUTH (p_username in number, p_password in VARCHAR2)
return BOOLEAN
is
l_stored_password varchar2(4000);
l_count number;
blockX number;
begin

select count(*) into l_count
from users where upper(usr_name) = upper(p_username);

IF l_count > 0 then

select usr_password, usr_block into l_stored_password, blockX
from users where upper(usr_name) = upper(p_username);

if blockX <1 then
return FALSE;
end if;

if p_password = l_stored_password then
update users set usr_block=3
where upper(usr_name) = upper(p_username);
return TRUE;
else
update users set usr_block=usr_block-1
where upper(usr_name) = upper(p_username);
return FALSE;
end if;
ELSE
return FALSE;
END IF;

end;

Niby ok, ale użytkownik nie będzie wiedział o zablokowaniu konta. Co zatem należy zrobić aby stosowna informacja ukazała się na ekranie ?

Cała procedura składa się z kilku punktów, które kolejno polegają na dodaniu itemsa aplikacyjnego, zmiany procesu na stronie Login i 

1. Dodaj Item aplikacyjny AUTH_MESSAGE. W itemsie tym będzie przetrzymywana informacja dla użytkownika. Ważne aby zastrzec możliwość ustawiania wartości tej zmiennej z przeglądarki. Zaznacz opcję "Restricted" z listy Session State Protection.

2. Na stronie logowania dodaj nowy region typu HTML. Jako template regionu wybierz "No Template" natomiast w źródle wpisz "&AUTH_MESSAGE." Natomiast jako kondycje wyświetlania wybierz "Value of Item in Expression 1 Is NOT NULL" i wpisz w polu Expression1 wartość "AUTH_MESSAGE".  Region ten będzie odpowiadał za wyświetlanie informacji użytkownikowi. Kondycja zadba o to aby pokazywał się tylko w momencie gdy jest jakaś informacja do przekazania :)

3. Zmień Proces Login na stronie logowania na następujący:

declare
bresult BOOLEAN := FALSE;
begin


bresult := DBE_CUSTOM_AUTH(:P101_USERNAME,:P101_PASSWORD);

if bresult then

wwv_flow_custom_auth_std.post_login(
P_UNAME => :P101_USERNAME,
P_PASSWORD => :P101_PASSWORD,
P_SESSION_ID => v('APP_SESSION'),
P_FLOW_PAGE => :APP_ID||':'||nvl(:P101_PAGE_ID,'2')
);

else

owa_util.redirect_url('f?p=&APP_ID.:101:&SESSION.');
end if;
end;

W procesie tym na początku sprawdzane jest czy użytkownik podał prawidłowe dane do logowania. Jeśli tak to zostanie uruchomiona standardowa procedura logowania. Jeśli nie to użytkownik zostanie przekierowany na stronę 101 (logowania).

4. Na końcu trzeba tylko zająć się zmienną AUTH_MESSAGE. Wypełnianiem tej wartości zajmie się funkcja uwierzytelniająca. 

create or replace
FUNCTION CUSTOM_AUTH (p_username in number, p_password in VARCHAR2)
return BOOLEAN
is
l_stored_password varchar2(4000);
l_count number;
blockX number;
begin
select count(*) into l_count
from users where upper(usr_name) = upper(p_username);

IF l_count > 0 then
select usr_password, usr_block into l_stored_password, blockX
from users where upper(usr_name) = upper(p_username);

if blockX <1 then
apex_util.set_session_state('LOGIN_MESSAGE','Twoje konto zostało zablokowane.
Skontaktuj się z administratorem !');
return FALSE;
end if;

if p_password = l_stored_password then
update users set usr_block=3
where upper(usr_name) = upper(p_username);
return TRUE;
else
update users set usr_block=usr_block-1
where upper(usr_name) = upper(p_username);
apex_util.set_session_state('LOGIN_MESSAGE','Nieprawidowe Haslo lub id !!!');
return FALSE;
end if;
ELSE

apex_util.set_session_state('LOGIN_MESSAGE','Nieprawidowe Haslo lub id !!!');
return FALSE;
END IF;

end;

W funkcji zastosowano fukncje APEX Api która ustawia wartość podanego Itemsa aplikcaji. W tym przypadku jest to Items AUTH_MESSAGE. 

Wystarczy zatem zapisać funkcję aby sprawdzić rezultat.

Na końcu można się pokusić o zmianę wyglądu naszego komunikatu tak aby był identyczny z tym w Template strony. Dla mojego Tematu Graficznego będzie to wyglądało tak:

<div class="t16notification" id="MESSAGE">
<img src="#IMAGE_PREFIX#delete.gif" onclick="$x_Remove('MESSAGE')" style="float:right;" class="pb" alt="" />
&AUTH_MESSAGE.
</div>

Gotowe. Teraz można dodawać inne opcje które pozwolą na poinformowanie uzytkownika o wszystkim co nas trapi i uwiera. Ba! Można pokusić się o personalizację komunikatu :) jak nizej:

19 komentarze:

  1. Dzięki za fajne wyjaśnienie. Generalnie super strona - znałem już wcześniej oracle'a ale tylko od strony administracji (OCP DBA:) a teraz okazuje się, że mogę pisać aplikacje :)
    Mam jedno pytanko - kiedyś ktoś tu wspominał o utworzeniu polskiego forum APEX-owego. Jak sprawa stoi?
    OdpowiedzUsuń na zawsze
  2. Witam

    Świetnie, że Blog się podoba i przydaje. Co do forum, to będzie na pewno, kwestia tylko znalezienia kilku dni aby je oprogramować. Jako, że Blog jest o Oracle Apex to nie wyobrażamy sobie inaczej, że forum będzie wykorzystywało jakiś darmowy silnik nie apex-owy. Dlatego musimy kilka dni wygospodarować na jego stworzenie.
    OdpowiedzUsuń na zawsze
  3. Super, czekam zatem na jakieś info :)

    pozdrawiam,
    Krystian
    OdpowiedzUsuń na zawsze
  4. Mam pytanie z innej beczki. Mam dwie tabele: faktury i linie faktur. Utworzyłem formularz master-detail. I wszystko jest bardzo ładnie, mam formularz ze wszystkimi fakturami, mogę wejść w edycje danych faktury, gdzie widzę na jednym ekranie dane ogólne + wszystkie linie faktury. Mogę też dodać/zmienić dane związane z liniami. Czyli działa jak powinno.

    Chciałbym jednak zmienić nieco zachowanie formularza - otóż chciałbym by formularz edycji linii faktury otwierał się jako popup. Zmieniłem co prawda template z 'Application default' na 'Popup', ale nie przyniosło to pożądanych rezultatów. Any idea??
    OdpowiedzUsuń na zawsze
  5. A próbowałeś wstawić w kolumnie z linkiem w Link Attributes:
    target="_blank"
    ?
    OdpowiedzUsuń na zawsze
  6. Próbowałem, po dodaniu wpisu kliknięcie powoduje otwarcie nowego okna, lecz nie jest to popup :( Różnica jest taka, że jak okno otwiera się jako popup (np. formant kalendarz) to otwiera się nowe okno. A w moim przypadku otwiera się po prostu kolejna zakładka...
    OdpowiedzUsuń na zawsze
  7. Nie wiem czemu ale nie mogę dodać komentarza, prawdopodobnie dlatego ze dużo jest kodu. Zaraz jeszcze raz spróbuję.
    OdpowiedzUsuń na zawsze
  8. Dokładnie wywraca się jak jest jakiś kod. Poniżej moja sugestia.

    Tak na szybko to przychodni mi jeszcze jeden pomysł. Może spróbować zmienić raport na którym jest link tak, żeby można było w źródle -> Select-cie stworzyć linka i tam dołożyć javscript.
    --No i niestety tu nie moge podać kodu, taki mi błąd wyskakuje bX-47qz89
    OdpowiedzUsuń na zawsze
  9. Robert,
    zmień sposób wywoływania formularza na URL i w miejsce url-a wstaw:
    javascript:popUp2(f?'p=&APP_ID.:nr_formularza:&SESSION.:::::')
    OdpowiedzUsuń na zawsze
  10. jeżeli master to jakiś raport to ja bym proponował link zrobić jako wstawienie kodu html (znacznik < a >) i wywolac funkcje javascriptowa w ktorej ladnie mozna pobrac dane z itemow, lub je tam przekazac przy wywolywaniu funkcji .

    nastepnie wywloac popup przy uzyciu funkcji window.open z szeregiem parametrow jednoczesnie po chwili mozna przeladowac strone glowna, podswietlic sobie zaznaczony element innym kolorkiem i rozne inne tego typu bajerki.
    OdpowiedzUsuń na zawsze
  11. A ja troche od innej strony.
    Czy nie lepiej jednak wszystko wyświetlać na jednej stronie ? Master, detail i jeszcze raport z master ? Oczywiście nie wszystko od razu ale w zależności od akcji wysyłać odpowiedni REQUEST i wyświetlać lub nie dany region. Przy naszych projektach właśnie takie stosujemy podejście i całkiem dobrze się sprawdza.
    OdpowiedzUsuń na zawsze
  12. ja stosuję oba podejścia. Zależy od liczby elementów na formularzach. W jednym projekcie sam formularz ledwo mieści się przy rozdzielczości x800, a dużo przyjemniej moim zdaniem ogląda się właśnie popup i jednocześnie pod nim mamy master z zaznaczonym wybranym elementem. W erze coraz częściej spotykanych idwóch monitorów na stanowisko - pomysł jak znalazł. no, ale jeżeli formularz to 4-5 itemów albo jakiś mały tabular form to jasne, że bez sensu robienie popupów
    OdpowiedzUsuń na zawsze
  13. a pytanie z innej beczki - wiecie jak blokować status bar by się nie wyświetlał,gdzieś tam przeczytałem, że zależy to od przeglądarki, ale pewnie da się też to jakoś przyblokować. Nie działa mi (nie wiem w sumie czemu) session state protection ustawione na checksum - od kiedy zastosowalem ajaxa z plsqlem on-demand, ale na forum nikt sie nie spotkal z podobnym problemem to chcialbym chociaz pobloowac dolny status bar by tamnie bylo widac jakie javascriptowe funkcje sa wywolywane...
    OdpowiedzUsuń na zawsze
  14. Widzę, że z komentarzy się powoli forum tworzy :)

    Mam pytanie dotyczące licencji na bazę danych. Jak to jest liczone w przypadku APEX-a? W Oracle'u można licencjonować się per named user. Czy tu chodzi o fizycznych użytkowników. Jeżeli schemat autoryzacji jest oparty na tabeli w bazie danych, to nazwanych użytkowników nie będzie zbyt wielu.

    Ma ktoś jakieś doświadczenia?
    OdpowiedzUsuń na zawsze
  15. Powiedziałby tak, APEX-a nie ma co w to mieszać. Licencjonowanie jest na bazę więc jeżeli jest na Named User to nie chodzi tu o użytkownika bazowego (schemat), tylko wszyscy użytkownicy a nawet urządzenia, którzy/które korzystają z systemu poprzez odbieranie lub wprowadzanie danych do bazy danych, bez względu czy system opiera sie o autoryzacje bazy danych czy własną.
    OdpowiedzUsuń na zawsze
  16. Dzięki za info. Zatem do momentu, aż nie przestanie wystarczać zasobów zostajemy przy XE :)

    pozdrawiam
    OdpowiedzUsuń na zawsze
  17. @Przemek Pytlak
    Czy możesz problem bardziej szczegółowo opisać ??

    Może być na e-mail :)

    pozdrawiam
    piotr
    OdpowiedzUsuń na zawsze
  18. Spróbuję powtórzyć ścieżkę postępowania w ciągu tygodnia dwóch i sie odezwę. Problem był chyba w listopadzie zeszłego roku, więc szczegóły wyleciały już z głowy, ale pamiętam, że podążając informacjami z blogu Denese'a:

    http://deneskubicek.blogspot.com/2009/06/how-to-debug-ajax-and-on-demand.html

    miałem jakiś problem z punktem numer 4, który wystepował dopóki nie zlikwidowałem session state protection.

    Ps. Jakbys jeszcze podał e-mail to wysłał bym info w taki właśnie sposób, niestety ani na blogu, szczegółach profilu i stronce dbe nie widzę adresu. Moge sie tylko domyślać że jest to np piotrjasinski@dbe.pl ;)
    OdpowiedzUsuń na zawsze
  19. Rzeczywiście mój adres e-mail nie był dostępny, Teraz jest więc jak coś to nie krępuj się :)

    pozdrawiam
    Piotr
    OdpowiedzUsuń na zawsze