Quando si utilizza Spring Security, qual è il modo corretto di ottenere informazioni sul nome utente corrente (ovvero SecurityContext) in un bean?

StackOverflow https://stackoverflow.com/questions/248562

Domanda

Ho un'app Web Spring MVC che utilizza Spring Security. Voglio sapere il nome utente dell'utente attualmente connesso. Sto usando lo snippet di codice indicato di seguito. È questo il modo accettato?

Non mi piace avere una chiamata a un metodo statico all'interno di questo controller - che sconfigge l'intero scopo di Spring, IMHO. Esiste un modo per configurare l'app in modo che venga iniettato l'attuale SecurityContext o l'attuale autenticazione?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
È stato utile?

Soluzione

Se stai usando Spring 3 , il modo più semplice è:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }

Altri suggerimenti

Molto è cambiato nel mondo primaverile da quando è stata data una risposta a questa domanda. La primavera ha semplificato il coinvolgimento dell'utente corrente in un controller. Per altri bean, Spring ha adottato i suggerimenti dell'autore e semplificato l'iniezione di "SecurityContextHolder". Maggiori dettagli sono nei commenti.


Questa è la soluzione con cui ho finito. Invece di usare SecurityContextHolder nel mio controller, voglio iniettare qualcosa che usa SecurityContextHolder sotto il cofano, ma estrae quella classe singleton dal mio codice. Non ho trovato alcun modo per farlo se non rotolare la mia interfaccia, in questo modo:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Ora, il mio controller (o qualunque POJO) sarebbe simile a questo:

public class FooController {

  private final SecurityContextFacade securityContextFacade;

  public FooController(SecurityContextFacade securityContextFacade) {
    this.securityContextFacade = securityContextFacade;
  }

  public void doSomething(){
    SecurityContext context = securityContextFacade.getContext();
    // do something w/ context
  }

}

E, poiché l'interfaccia è un punto di disaccoppiamento, i test unitari sono semplici. In questo esempio uso Mockito:

public class FooControllerTest {

  private FooController controller;
  private SecurityContextFacade mockSecurityContextFacade;
  private SecurityContext mockSecurityContext;

  @Before
  public void setUp() throws Exception {
    mockSecurityContextFacade = mock(SecurityContextFacade.class);
    mockSecurityContext = mock(SecurityContext.class);
    stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
    controller = new FooController(mockSecurityContextFacade);
  }

  @Test
  public void testDoSomething() {
    controller.doSomething();
    verify(mockSecurityContextFacade).getContext();
  }

}

L'implementazione predefinita dell'interfaccia è simile alla seguente:

public class SecurityContextHolderFacade implements SecurityContextFacade {

  public SecurityContext getContext() {
    return SecurityContextHolder.getContext();
  }

  public void setContext(SecurityContext securityContext) {
    SecurityContextHolder.setContext(securityContext);
  }

}

E, infine, la configurazione Spring di produzione si presenta così:

<bean id="myController" class="com.foo.FooController">
     ...
  <constructor-arg index="1">
    <bean class="com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Sembra più che un po 'sciocco che Spring, un contenitore di iniezione di dipendenza di tutte le cose, non abbia fornito un modo per iniettare qualcosa di simile. Capisco che SecurityContextHolder è stato ereditato da acegi, ma comunque. Il fatto è che sono così vicini - se solo SecurityContextHolder avesse un getter per ottenere l'istanza SecurityContextHolderStrategy sottostante (che è un'interfaccia), potresti iniettarla. In effetti, ho anche aperto un problema di Jira in tal senso.

Un'ultima cosa: ho appena cambiato sostanzialmente la risposta che avevo qui prima. Controlla la cronologia se sei curioso ma, come mi ha sottolineato un collega, la mia risposta precedente non avrebbe funzionato in un ambiente multi-thread. Il SecurityContextHolderStrategy sottostante utilizzato da SecurityContextHolder è, per impostazione predefinita, un'istanza di ThreadLocalSecurityContextHolderStrategy , che memorizza SecurityContext in un ThreadLocal . Pertanto, non è necessariamente una buona idea iniettare il SecurityContext direttamente in un bean al momento dell'inizializzazione - potrebbe essere necessario recuperarlo da ThreadLocal ogni volta, in un multi -threaded environment, quindi viene recuperato quello corretto.

Sono d'accordo sul fatto che dover interrogare SecurityContext per l'utente corrente puzza, sembra un modo molto non-Spring per gestire questo problema.

Ho scritto un "helper" statico " classe per affrontare questo problema; è sporco in quanto è un metodo globale e statico, ma ho pensato in questo modo se cambiassimo qualcosa legato alla sicurezza, almeno devo solo cambiare i dettagli in un unico posto:

/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
* 
* @return User object for the currently logged in user, or null if no User
*         is logged in.
*/
public static User getCurrentUser() {

    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()

    if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();

    // principal object is either null or represents anonymous user -
    // neither of which our domain User object can represent - so return null
    return null;
}


/**
 * Utility method to determine if the current user is logged in /
 * authenticated.
 * <p>
 * Equivalent of calling:
 * <p>
 * <code>getCurrentUser() != null</code>
 * 
 * @return if user is logged in
 */
public static boolean isLoggedIn() {
    return getCurrentUser() != null;
}

Per farlo apparire nelle tue pagine JSP, puoi usare il tag Spring Security Lib:

http://static.springsource.org/spring-security /site/docs/3.0.x/reference/taglibs.html

Per utilizzare uno qualsiasi dei tag, è necessario che il taglib di sicurezza sia dichiarato nel proprio JSP:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Quindi in una pagina jsp fai qualcosa del genere:

<security:authorize access="isAuthenticated()">
    logged in as <security:authentication property="principal.username" /> 
</security:authorize>

<security:authorize access="! isAuthenticated()">
    not logged in
</security:authorize>

NOTA: come menzionato nei commenti di @ SBerg413, dovrai aggiungere

  

Usa-espressioni = " vero "

in " http " tag nella configurazione di security.xml affinché funzioni.

Se stai utilizzando Spring Security ver > = 3.2, puoi utilizzare l'annotazione @AuthenticationPrincipal :

@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
    String currentUsername = currentUser.getUsername();
    // ...
}

Qui CustomUser è un oggetto personalizzato che implementa UserDetails che viene restituito da un UserDetailsService personalizzato.

Ulteriori informazioni sono disponibili in @ AuthenticationPrincipal dei documenti di riferimento di Spring Security.

Ricevo l'utente autenticato da HttpServletRequest.getUserPrincipal ();

Esempio:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;

import foo.Form;

@Controller
@RequestMapping(value="/welcome")
public class IndexController {

    @RequestMapping(method=RequestMethod.GET)
    public String getCreateForm(Model model, HttpServletRequest request) {

        if(request.getUserPrincipal() != null) {
            String loginName = request.getUserPrincipal().getName();
            System.out.println("loginName : " + loginName );
        }

        model.addAttribute("form", new Form());
        return "welcome";
    }
}

In Spring 3+ hai le seguenti opzioni.

Opzione 1:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserNameByPrincipal(Principal principal) {
    return principal.getName();
}

Opzione 2:

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
    return authentication.getName();
}

Opzione 3:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserByHTTPRequest(HttpServletRequest request) {
    return request.getUserPrincipal().getName();

}

Opzione 4: fantasia: Dai un'occhiata a questo per maggiori dettagli

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}

Sì, la statica è generalmente negativa - in generale, ma in questo caso, la statica è il codice più sicuro che puoi scrivere. Poiché il contesto di sicurezza associa un principale al thread attualmente in esecuzione, il codice più sicuro accederà allo statico dal thread il più direttamente possibile. Nascondere l'accesso dietro una classe wrapper che viene iniettata fornisce a un attaccante più punti per attaccare. Non avrebbero bisogno di accedere al codice (che avrebbero difficoltà a cambiare se il jar fosse firmato), hanno solo bisogno di un modo per sovrascrivere la configurazione, che può essere fatta in fase di esecuzione o facendo scivolare un po 'di XML sul percorso di classe. Anche l'utilizzo dell'iniezione di annotazioni nel codice firmato sarebbe sovrascrivibile con XML esterno. Tale XML potrebbe iniettare il sistema in esecuzione con un'entità errata. Questo è probabilmente il motivo per cui Spring sta facendo qualcosa di così non-Spring in questo caso.

Vorrei solo fare questo:

request.getRemoteUser();

Per l'ultima app Spring MVC che ho scritto, non ho iniettato il supporto SecurityContext, ma avevo un controller di base con due metodi di utilità correlati a questo ... isAuthenticated () & amp; getUsername (). Internamente eseguono il metodo statico chiamato da te descritto.

Almeno allora è solo una volta se devi rifattorizzare in seguito.

È possibile utilizzare l'approccio Spring AOP. Ad esempio, se si dispone di un servizio, è necessario conoscere l'entità corrente. È possibile introdurre un'annotazione personalizzata, ad esempio @Principal, che indica che questo servizio dovrebbe dipendere dall'entità principale.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Quindi, nel tuo consiglio, che penso debba estendere MethodBeforeAdvice, controlla che quel particolare servizio abbia l'annotazione @Principal e inietti nome principale, oppure impostalo invece su 'ANONYMOUS'.

L'unico problema è che anche dopo l'autenticazione con Spring Security, il bean utente / principale non esiste nel contenitore, quindi l'iniezione di dipendenza sarà difficile. Prima di utilizzare Spring Security, dovevamo creare un bean con ambito sessione che avesse l'attuale Principal, iniettandolo in un "Servizio di assistenza". e quindi iniettare quel servizio nella maggior parte degli altri servizi nell'applicazione. Quindi quei Servizi chiamerebbero authService.getCurrentUser () per ottenere l'oggetto. Se hai un posto nel tuo codice in cui ottieni un riferimento allo stesso principale nella sessione, puoi semplicemente impostarlo come proprietà sul tuo bean con ambito di sessione.

Prova questo

  

Autenticazione autenticazione =   . SecurityContextHolder.getContext () getAuthentication ();
  Stringa userName = authentication.getName ();

La migliore soluzione se stai usando Spring 3 e hai bisogno dell'entità autenticata nel tuo controller è fare qualcosa del genere:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;

    @Controller
    public class KnoteController {
        @RequestMapping(method = RequestMethod.GET)
        public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {

            if (authToken instanceof UsernamePasswordAuthenticationToken) {
                user = (User) authToken.getPrincipal();
            }
            ...

    }

Sto usando l'annotazione @AuthenticationPrincipal nelle classi @Controller e in quelle annotate @ControllerAdvicer . Es:.

@ControllerAdvice
public class ControllerAdvicer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);


    @ModelAttribute("userActive")
    public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
    {
        return currentUser;
    }
}

Dove UserActive è la classe che utilizzo per i servizi degli utenti registrati e si estende da org.springframework.security.core.userdetails.User . Qualcosa del tipo:

public class UserActive extends org.springframework.security.core.userdetails.User
{

    private final User user;

    public UserActive(User user)
    {
        super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
        this.user = user;
    }

     //More functions
}

Davvero facile.

Definisci Principal come dipendenza nel metodo del tuo controller e spring inserirà l'utente autenticato nel tuo metodo al momento dell'invocazione.

Mi piace condividere il mio modo di supportare i dettagli dell'utente sulla pagina del freemarker. Tutto è molto semplice e funziona perfettamente!

Devi solo inserire la richiesta di autenticazione su default-target-url (pagina dopo il form-login) Questo è il mio metodo Controler per quella pagina:

@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
    showRequestLog("monitoring");


    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    //create a new session
    HttpSession session = request.getSession(true);
    session.setAttribute("username", userName);

    return new ModelAndView(catalogPath + "monitoring");
}

E questo è il mio codice ftl:

<@security.authorize ifAnyGranted="ROLE_ADMIN, ROLE_USER">
<p style="padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize> 

Ed ecco fatto, il nome utente apparirà su ogni pagina dopo l'autorizzazione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top