Wykonywanie uwierzytelniania użytkownika w Java EE / JSF przy użyciu j_security_check

156

Zastanawiam się, jakie jest obecne podejście do uwierzytelniania użytkowników w aplikacji internetowej korzystającej z JSF 2.0 (i czy jakieś komponenty istnieją) i podstawowych mechanizmów Java EE 6 (logowanie / sprawdzanie uprawnień / wylogowania) z informacjami o użytkowniku przechowywanymi w JPA jednostka. Samouczek dotyczący Oracle Java EE jest nieco rzadki (obsługuje tylko serwlety).

Dzieje się tak bez korzystania z całego innego frameworka, takiego jak Spring-Security (acegi) lub Seam, ale starając się, miejmy nadzieję, trzymać się nowej platformy Java EE 6 (profil sieciowy), jeśli to możliwe.

ngeek
źródło

Odpowiedzi:

85

Po przeszukaniu sieci i wypróbowaniu wielu różnych sposobów, oto co zasugerowałbym dla uwierzytelniania Java EE 6:

Skonfiguruj dziedzinę zabezpieczeń:

W moim przypadku miałem użytkowników w bazie danych. Więc podążyłem za tym postem na blogu, aby utworzyć JDBC Realm, który może uwierzytelniać użytkowników na podstawie nazwy użytkownika i haszowanych haseł MD5 w mojej tabeli bazy danych:

http://blog.gamatam.com/2009/11/jdbc-realm-setup-with-glassfish-v3.html

Uwaga: post mówi o użytkowniku i tabeli grupowej w bazie danych. Miałem klasę użytkownika z atrybutem wyliczenia UserType zmapowanym za pośrednictwem adnotacji javax.persistence do bazy danych. Skonfigurowałem dziedzinę z tą samą tabelą dla użytkowników i grup, używając kolumny userType jako kolumny grupy i działało dobrze.

Użyj uwierzytelniania formularza:

Nadal postępując zgodnie z powyższym wpisem na blogu, skonfiguruj swoje pliki web.xml i sun-web.xml, ale zamiast używać uwierzytelniania BASIC, użyj FORMULARZA (właściwie nie ma znaczenia, którego używasz, ale ostatecznie użyłem FORMULARZA). Użyj standardowego kodu HTML, a nie JSF.

Następnie użyj powyższej wskazówki BalusC dotyczącej leniwego inicjowania informacji o użytkowniku z bazy danych. Zasugerował zrobienie tego w zarządzanej fasoli, uzyskując dyrektora z kontekstu twarzy. Zamiast tego użyłem stanowego komponentu bean sesji do przechowywania informacji o sesji dla każdego użytkownika, więc wstrzyknąłem kontekst sesji:

 @Resource
 private SessionContext sessionContext;

Z podmiotem głównym mogę sprawdzić nazwę użytkownika i za pomocą EJB Entity Manager pobrać informacje o użytkowniku z bazy danych i przechowywać w moim SessionInformationEJB.

Wyloguj:

Rozejrzałem się też za najlepszym sposobem wylogowania. Najlepsze, jakie znalazłem, to użycie serwletu:

 @WebServlet(name = "LogoutServlet", urlPatterns = {"/logout"})
 public class LogoutServlet extends HttpServlet {
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   HttpSession session = request.getSession(false);

   // Destroys the session for this user.
   if (session != null)
        session.invalidate();

   // Redirects back to the initial page.
   response.sendRedirect(request.getContextPath());
  }
 }

Chociaż moja odpowiedź jest naprawdę spóźniona, biorąc pod uwagę datę pytania, mam nadzieję, że pomoże to innym osobom, które trafiają tutaj z Google, tak jak ja.

Cześć,

Vítor Souza

Vítor E. Silva Souza
źródło
15
Mała rada: używasz request.getSession (false) i wywołujesz do tego funkcję invalidate (). request.getSession (false) może zwrócić wartość null, jeśli nie ma sesji. Lepiej sprawdź, czy najpierw jest null;)
Arjan Tijms
@Vitor: Cześć… Czy chciałbyś coś powiedzieć o tym, kiedy dobrze jest przejść od zabezpieczeń opartych na kontenerach do alternatyw takich jak shiro lub inne? Zobacz bardziej skoncentrowane pytanie tutaj: stackoverflow.com/questions/7782720/…
Rajat Gupta
Wygląda na to, że Glassfish JDBC Realm nie obsługuje przechowywania zasolonych skrótów haseł. Czy naprawdę jest to najlepsza praktyka w takim przypadku?
Lii
Przepraszam, nie mogę ci pomóc. Nie jestem ekspertem od szklistych ryb. Może zadaj to pytanie w nowym wątku, aby zobaczyć, co mówią ludzie?
Vítor E. Silva Souza
1
Lii, możesz pracować z solą, używając pojemnika na szklaną rybę. Skonfiguruj swój healm tak, aby nie używał żadnego skrótu. Porówna zwykłą wartość, którą wstawisz dla hasła HttpServletResponse#login(user, password), w ten sposób możesz po prostu pobrać z DB sól użytkownika, iteracje i cokolwiek, czego używasz do solenia, zaszyfrować hasło wprowadzone przez użytkownika za pomocą tej soli, a następnie poprosić kontener o uwierzytelnienie HttpServletResponse#login(user, password).
emportella
152

Przypuszczam, że chcesz uwierzytelniać oparte na formularzach przy użyciu deskryptorów wdrażania i j_security_check.

Można również zrobić to w JSF tylko przy użyciu tych samych predefinied nazwy pól j_usernamei j_passwordjak wykazano w samouczku.

Na przykład

<form action="j_security_check" method="post">
    <h:outputLabel for="j_username" value="Username" />
    <h:inputText id="j_username" />
    <br />
    <h:outputLabel for="j_password" value="Password" />
    <h:inputSecret id="j_password" />
    <br />
    <h:commandButton value="Login" />
</form>

Możesz wykonać leniwe ładowanie w Usergetterze, aby sprawdzić, czy Userjest już zalogowany, a jeśli nie, to sprawdź, czy Principaljest obecny w żądaniu, a jeśli tak, to pobierz Userskojarzony z j_username.

package com.stackoverflow.q2206911;

import java.io.IOException;
import java.security.Principal;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.FacesContext;

@ManagedBean
@SessionScoped
public class Auth {

    private User user; // The JPA entity.

    @EJB
    private UserService userService;

    public User getUser() {
        if (user == null) {
            Principal principal = FacesContext.getCurrentInstance().getExternalContext().getUserPrincipal();
            if (principal != null) {
                user = userService.find(principal.getName()); // Find User by j_username.
            }
        }
        return user;
    }

}

UserJest oczywiście dostępny w JSF EL przez#{auth.user} .

Aby się wylogować, wykonaj a HttpServletRequest#logout()(i ustaw Userna null!). Możesz uzyskać uchwyt HttpServletRequestw JSF przez ExternalContext#getRequest(). Możesz także całkowicie unieważnić sesję.

public String logout() {
    FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
    return "login?faces-redirect=true";
}

Jeśli chodzi o resztę (definiowanie użytkowników, ról i ograniczeń w deskryptorze wdrażania i dziedzinie), po prostu postępuj zgodnie z samouczkiem Java EE 6 i dokumentacją servletcontainer w zwykły sposób.


Aktualizacja : możesz także użyć nowego Servlet 3.0 HttpServletRequest#login()do automatycznego logowania zamiast używania, j_security_checkktóre samo w sobie może nie być osiągalne dla dyspozytora w niektórych kontenerach serwletów. W tym przypadku można użyć fullworthy formularza JSF i fasoli z usernameoraz passwordwłaściwości i loginmetody, które wyglądają tak:

<h:form>
    <h:outputLabel for="username" value="Username" />
    <h:inputText id="username" value="#{auth.username}" required="true" />
    <h:message for="username" />
    <br />
    <h:outputLabel for="password" value="Password" />
    <h:inputSecret id="password" value="#{auth.password}" required="true" />
    <h:message for="password" />
    <br />
    <h:commandButton value="Login" action="#{auth.login}" />
    <h:messages globalOnly="true" />
</h:form>

I ten zarządzany komponent bean z zakresem widoku, który również pamięta początkowo żądaną stronę:

@ManagedBean
@ViewScoped
public class Auth {

    private String username;
    private String password;
    private String originalURL;

    @PostConstruct
    public void init() {
        ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
        originalURL = (String) externalContext.getRequestMap().get(RequestDispatcher.FORWARD_REQUEST_URI);

        if (originalURL == null) {
            originalURL = externalContext.getRequestContextPath() + "/home.xhtml";
        } else {
            String originalQuery = (String) externalContext.getRequestMap().get(RequestDispatcher.FORWARD_QUERY_STRING);

            if (originalQuery != null) {
                originalURL += "?" + originalQuery;
            }
        }
    }

    @EJB
    private UserService userService;

    public void login() throws IOException {
        FacesContext context = FacesContext.getCurrentInstance();
        ExternalContext externalContext = context.getExternalContext();
        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();

        try {
            request.login(username, password);
            User user = userService.find(username, password);
            externalContext.getSessionMap().put("user", user);
            externalContext.redirect(originalURL);
        } catch (ServletException e) {
            // Handle unknown username/password in request.login().
            context.addMessage(null, new FacesMessage("Unknown login"));
        }
    }

    public void logout() throws IOException {
        ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
        externalContext.invalidateSession();
        externalContext.redirect(externalContext.getRequestContextPath() + "/login.xhtml");
    }

    // Getters/setters for username and password.
}

W ten sposób Userplik jest dostępny w JSF EL przez #{user}.

BalusC
źródło
1
Zaktualizowałem pytanie, j_security_checkdodając zastrzeżenie, że wysyłanie do może nie działać na wszystkich kontenerach serwletów.
BalusC
1
Odsyłacz do samouczka Java dotyczącego korzystania z zabezpieczeń programowych w aplikacjach internetowych: java.sun.com/javaee/6/docs/tutorial/doc/gjiie.html (przy użyciu serwletów): W klasie serwletów można użyć: @WebServlet(name="testServlet", urlPatterns={"/ testServlet "}) @ServletSecurity(@HttpConstraint(rolesAllowed = {"testUser", "admin”})) I na metodę według poziom: @ServletSecurity(httpMethodConstraints={ @HttpMethodConstraint("GET"), @HttpMethodConstraint(value="POST", rolesAllowed={"testUser"})})
ngeek
3
A twoim celem jest…? Czy ma to zastosowanie w JSF? Cóż, w JSF jest tylko jeden serwlet, którego FacesServletnie możesz (i nie chcesz) modyfikować.
BalusC
1
@BalusC - Kiedy mówisz, że powyższe jest najlepszym sposobem, masz na myśli użycie j_security_check lub programistycznego logowania?
simgineer
3
@simgineer: żądany adres URL jest dostępny jako atrybut żądania o nazwie określonej przez RequestDispatcher.FORWARD_REQUEST_URI. Atrybuty żądania są w formacie JSF dostępne przez ExternalContext#getRequestMap().
BalusC,
7

Należy wspomnieć, że jest to opcja, aby całkowicie pozostawić kwestie uwierzytelniania kontrolerowi frontowemu, np. Serwerowi WWW Apache i zamiast tego ocenić HttpServletRequest.getRemoteUser (), która jest reprezentacją JAVA dla zmiennej środowiskowej REMOTE_USER. Pozwala to również na zaawansowane projekty logowania, takie jak uwierzytelnianie Shibboleth. Filtrowanie żądań do kontenera serwletów przez serwer WWW jest dobrym rozwiązaniem dla środowisk produkcyjnych, często do tego celu używa się mod_jk.

Matthias Ronge
źródło
4

Problem HttpServletRequest.login nie ustawia stanu uwierzytelniania w sesji został rozwiązany w wersji 3.0.1. Zaktualizuj Glassfish do najnowszej wersji i gotowe.

Aktualizacja jest dość prosta:

glassfishv3/bin/pkg set-authority -P dev.glassfish.org
glassfishv3/bin/pkg image-update
Hans Bacher
źródło
4
link jest uszkodzony. do którego problemu się odnosiliście?
simgineer