Spring Security kullanırken, bir çekirdekte geçerli kullanıcı adı (yani SecurityContext) bilgilerini almanın doğru yolu nedir?


288

Ben Bahar Güvenliği kullanan bir Bahar MVC web uygulaması var. Şu anda oturum açmış olan kullanıcının kullanıcı adını bilmek istiyorum. Aşağıda verilen kod snippet'ini kullanıyorum. Kabul edilen yol bu mu?

Bu denetleyicinin içindeki statik bir yönteme çağrı yapmaktan hoşlanmam - Spring, IMHO'nun tüm amacını bozar. Uygulamayı geçerli SecurityContext'i veya geçerli Kimlik Doğrulaması'nı enjekte edecek şekilde yapılandırmanın bir yolu var mı?

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

Süper sınıf olarak bir denetleyiciniz (güvenli denetleyici) neden kullanıcıyı SecurityContext'ten alıp bu sınıfın içinde bir örnek değişken olarak ayarlamıyorsunuz? Bu şekilde güvenli denetleyiciyi genişlettiğinizde, tüm sınıfınız geçerli bağlamın Kullanıcı ilkesine erişebilir.
Dehan de Croos

Yanıtlar:


259

İlkbahar 3'ü kullanıyorsanız , en kolay yol:

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

     final String currentUser = principal.getName();

 }

69

Bu soru cevaplandığından beri Bahar dünyasında çok şey değişti. Spring, mevcut kullanıcıyı bir denetleyiciye almayı basitleştirdi. Diğer fasulye için Spring, yazarın önerilerini kabul etti ve 'SecurityContextHolder' enjeksiyonunu basitleştirdi. Daha fazla ayrıntı yorumlarda yer almaktadır.


Sonuçta bu benim çözümüm. SecurityContextHolderDenetleyicimde kullanmak yerine SecurityContextHolder, kaputun altında kullanan ancak o singleton benzeri sınıfı kodumdan soyutlayan bir şey enjekte etmek istiyorum . Bunu kendi arayüzümü yuvarlamak dışında bunu yapmanın bir yolunu bulamadım, şöyle:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Şimdi, denetleyicim (veya POJO ne olursa olsun) şöyle görünecektir:

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
  }

}

Arayüzün ayrılma noktası olması nedeniyle, birim testi basittir. Bu örnekte Mockito kullanıyorum:

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();
  }

}

Arayüzün varsayılan uygulaması şu şekildedir:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

Ve son olarak, üretim Baharı yapılandırması şöyle görünür:

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

Her şeyin bağımlılık enjeksiyon kabı olan Spring'in benzer bir şey enjekte etmek için bir yol sağlamadığı biraz aptalca görünüyor. Anlıyorum SecurityContextHolder, acegi'den miras kaldı, ama yine de. Mesele şu ki, çok yakınlar - sadece SecurityContextHoldertemel SecurityContextHolderStrategyörneği (bir arayüz olan) almak için bir alıcı varsa , bunu enjekte edebilirsiniz. Aslında bu konuda bir Jira sorunu bile açtım .

Son bir şey daha önce burada verdiğim cevabı büyük ölçüde değiştirdim. Merak ediyorsanız geçmişi kontrol edin, ancak bir iş arkadaşınızın bana işaret ettiği gibi, önceki cevabım çok iş parçacıklı bir ortamda işe yaramayacaktı. Altta SecurityContextHolderStrategykullanılan SecurityContextHoldervarsayılan, bir örneği gereği, ThreadLocalSecurityContextHolderStrategymağazalar SecurityContexts a ThreadLocal. Bu nedenle, SecurityContextbaşlatma zamanında doğrudan bir fasülyeye enjekte etmek iyi bir fikir değildir - ThreadLocalçok iş parçacıklı bir ortamda her seferinde geri alınması gerekebilir , böylece doğru olanı alınır.


1
Çözümünüzü beğendim - Spring'deki fabrika yöntemi desteğinin akıllıca kullanılması. Bununla birlikte, denetleyici nesnesi web isteğine dahil edildiğinden, bu sizin için çalışıyor. Denetleyici çekirdeğinin kapsamını yanlış bir şekilde değiştirirseniz, bu kırılacaktır.
Paul Morie

2
Önceki iki yorum, yeni değiştirdiğim eski, yanlış bir cevaba işaret ediyor.
Scott Bale

12
Mevcut Bahar sürümü için hala önerilen çözüm bu mu? Sadece kullanıcı adını almak için çok fazla kod gerektirdiğine inanamıyorum.
Ta Sas

6
Spring Security 3.0.x kullanıyorsanız, jira.springsource.org/browse/SEC-1188'e giriş yaptığım JIRA sorununda önerimi uyguladılar, böylece artık SecurityContextHolderStrategy örneğini (SecurityContextHolder'dan) standart olarak doğrudan çekirdeğinize enjekte edebilirsiniz Yay konfigürasyonu.
Scott Bale

4
Lütfen tsunade21 yanıtına bakın. İlkbahar 3 şimdi java.security.Principal'ı kontrol cihazınızda bir yöntem argümanı olarak kullanmanıza izin veriyor
Patrick

22

Geçerli kullanıcı kokuyor SecurityContext sorgulamak zorunda, bu sorunu ele Bahar çok yol gibi görünüyor kabul ediyorum.

Bu sorunla başa çıkmak için statik bir "yardımcı" sınıf yazdım; Bu küresel ve statik bir yöntem olduğu için kirli, ama Güvenlik ile ilgili herhangi bir şeyi değiştirirsek, en azından sadece bir yerde ayrıntıları değiştirmek zorundayım:

/**
* 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;
}

22
SecurityContextHolder.getContext () olduğu sürece ve ikincisi güvenlik ayrıntılarını bir threadLocal içinde tuttuğu için threadsafe'dir. Bu kod hiçbir durum korumaz.
matt b

22

JSP sayfalarınızda görünmesini sağlamak için Spring Security Tag Lib'i kullanabilirsiniz:

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

Etiketlerden herhangi birini kullanmak için JSP'nizde güvenlik taglib'inin bildirilmiş olması gerekir:

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

Sonra bir jsp sayfasında böyle bir şey yapın:

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

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

NOT: @ SBerg413 tarafından yapılan yorumlarda belirtildiği gibi,

kullanım-ifadeler = "true"

Bunun için security.xml yapılandırmasındaki "http" etiketine gidin.


Bu muhtemelen Spring Security onaylı bir yol gibi görünüyor!
Nick Spacek

3
Bu yöntemin çalışması için, security.xml yapılandırmasındaki http etiketine use-expressions = "true" eklemeniz gerekir.
SBerg413

@ SBerg413 teşekkürler, cevabımı düzenleyeceğim ve önemli açıklamanızı ekleyeceğim!
Brad Parks

14

Spring Security ver> = 3.2 kullanıyorsanız @AuthenticationPrincipalek açıklamayı kullanabilirsiniz :

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

Burada, CustomUserbir özel UserDetailstarafından döndürülen uygulayan özel bir nesnedir UserDetailsService.

Daha fazla bilgi Spring Security başvuru belgelerinin @AuthenticationPrincipal bölümünde bulunabilir .


13

HttpServletRequest.getUserPrincipal () tarafından kimliği doğrulanmış kullanıcı olsun;

Misal:

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";
    }
}

Çözümünü seviyorum. Spring uzmanlarına: Bu güvenli ve iyi bir çözüm müdür?
marioosh

İyi bir çözüm değil. nullKullanıcının anonim olarak kimlik doğrulaması yapıldığını ( Spring Security XML'deki http> anonymousöğeler) alırsınız . SecurityContextHolderya SecurityContextHolderStrategyda doğru yol.
Nowaker

1
Böylece null request.getUserPrincipal ()! = Null olup olmadığını kontrol ettim.
digz6666

Filtrede boş mu
Alex78191

9

İlkbahar 3+'de aşağıdaki seçenekleriniz vardır.

Seçenek 1 :

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

Seçenek 2 :

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

Seçenek 3:

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

}

Seçenek 4: Süslü olan: Daha fazla ayrıntı için buna göz atın

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

1
3.2'den beri, bahar-güvenlik-web , bağlantınızdan gelen @CurrentUserözel gibi çalışır @ActiveUser.
Mike Partridge

@MikePartridge, ne dediğini bulamadım, herhangi bir bağlantı ?? veya daha fazla bilgi ??
azerafati

1
Benim hatam - Spring Security AuthenticationPrincipalArgumentResolver javadoc'u yanlış anladım . Orada @AuthenticationPrincipalözel bir @CurrentUseraçıklama ile sarma bir örnek gösterilmektedir . 3.2'den beri bağlantılı cevaptaki gibi özel bir argüman çözümleyici uygulamamıza gerek yok. Bu diğer cevabın daha fazla detayı var.
Mike Partridge

5

Evet, statikler genellikle kötüdür - genellikle, ancak bu durumda statik, yazabileceğiniz en güvenli koddur. Güvenlik bağlamı, bir Yöneticiyi şu anda çalışan iş parçacığıyla ilişkilendirdiğinden, en güvenli kod, iş parçacığından gelen statik değere mümkün olduğunca doğrudan erişir. Erişimi, enjekte edilen bir sarıcı sınıfın arkasına gizlemek, bir saldırgana daha fazla saldırı puanı sağlar. Koda erişmeleri gerekmeyecek (kavanoz imzalanırsa zor zamanlar değişecekti), sadece çalışma zamanında yapılabilecek veya bazı XML'i sınıfyoluna kaydırabilecek yapılandırmayı geçersiz kılmak için bir yola ihtiyaçları var. İmzalı kodda ek açıklama enjeksiyonu kullanmak bile harici XML ile geçersiz kılınabilir. Böyle bir XML, çalışan sistemi haydut bir müdürle enjekte edebilir.


5

Sadece bunu yapardım:

request.getRemoteUser();

1
Bu işe yarayabilir, ancak güvenilir bir şekilde değil. Javadoc'tan: "Kullanıcı adının sonraki her istekle gönderilip gönderilmeyeceği tarayıcıya ve kimlik doğrulama türüne bağlıdır." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale

3
Bu aslında bir Spring Security web uygulamasında uzak kullanıcı adını almanın geçerli ve çok basit bir yoludur. Standart filtre zinciri SecurityContextHolderAwareRequestFilter, isteği tamamlayan ve bu çağrıyı SecurityContextHolder.
Koyun Shaun

4

Yazdığım son Bahar MVC uygulaması için, SecurityContext sahibini enjekte etmedim, ancak bununla ilgili iki yardımcı yöntemim olan bir temel denetleyicim var ... isAuthenticated () & getUsername (). Dahili olarak tarif ettiğiniz statik yöntem çağrısını yaparlar.

En azından o zaman daha sonra refactor gerekiyorsa sadece bir yerde.


3

Bahar AOP yaklaşımını kullanabilirsiniz. Örneğin, bazı hizmetleriniz varsa, geçerli anaparayı bilmesi gerekir. Bu Hizmetin asıl olarak bağımlı olması gerektiğini belirten özel açıklama (@Principal) sunabilirsiniz.

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

Daha sonra, MethodBeforeAdvice'i genişletmesi gerektiğini düşündüğünüz tavsiyenizde, belirli hizmetin @Principal ek açıklaması olduğunu kontrol edin ve Ana adı enjekte edin veya bunun yerine 'ANONYMOUS' olarak ayarlayın.


Hizmet sınıfı içinde Müdür'e erişmem gerekiyor github'a tam bir örnek gönderebilir misiniz? Bahar AOP bilmiyorum, bu nedenle istek.
Rakesh Waghela

2

Tek sorun, Spring Security ile kimlik doğrulamasından sonra bile, kullanıcı / ana fasulyenin kapta mevcut olmamasıdır, bu nedenle bağımlılık enjekte etmek zor olacaktır. Spring Security kullanmadan önce mevcut Anapara sahip bir oturum kapsamlı fasulye oluşturduk, bunu bir "AuthService" içine enjekte ettikten sonra bu Hizmeti Uygulamadaki diğer hizmetlerin çoğuna enjekte edeceğiz. Böylece bu Hizmetler nesneyi almak için authService.getCurrentUser () yöntemini çağırır. Kodunuzda, oturumda aynı Anaparaya başvurduğunuz bir yer varsa, bunu oturum kapsamındaki fasulyenizde bir özellik olarak ayarlayabilirsiniz.


1

Bunu dene

Kimlik Doğrulama authentication = SecurityContextHolder.getContext (). GetAuthentication ();
String userName = authentication.getName ();


3
SecurityContextHolder.getContext () statik yöntemini çağırmak, tam olarak orijinal soruda şikayet ettiğim şeydir. Hiçbir şey cevaplamadınız.
Scott Bale

2
Bununla birlikte, belgelerin tam olarak önerdiği şey şudur : static.springsource.org/spring-security/site/docs/3.0.x/… Bundan kaçınarak ne yapıyorsunuz? Basit bir soruna karmaşık bir çözüm arıyorsunuz. En iyi ihtimalle - aynı davranışı elde edersiniz. En kötüsü, bir hata veya güvenlik deliği alırsınız.
Bob Kerns

2
@BobKerns Test için, kimlik doğrulamasını yerel bir iş parçacığına koymak yerine enjekte edebilmek daha temizdir.

1

Spring 3 kullanıyorsanız ve kontrol cihazınızda kimliği doğrulanmış prensibe ihtiyacınız varsa en iyi çözüm böyle bir şey yapmaktır:

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();
            }
            ...

    }

1
Neden parametre zaten UsernamePasswordAuthenticationToken türünde olduğunda UsernamePasswordAuthenticationToken kontrolünü yapıyorsunuz?
Scott Bale

(authToken instanceof UsernamePasswordAuthenticationToken), if (authToken! = null) işlevinin işlevsel eşdeğeridir. İkincisi biraz daha temiz olabilir, ancak aksi takdirde fark yoktur.
Mark

1

Ben kullanıyorum @AuthenticationPrincipaliçinde ek açıklama @Controllersınıfları yanı sıra @ControllerAdviceraçıklamalı olanlar. Ör .:

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


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

UserActiveKayıtlı kullanıcı hizmetleri için kullandığım sınıf nerede ve uzanıyor org.springframework.security.core.userdetails.User. Gibi bir şey:

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
}

Gerçekten kolay.


0

PrincipalDenetleyici yönteminizde bir bağımlılık olarak tanımlayın ve yay, geçerli kimliği doğrulanmış kullanıcıyı çağırma yönteminize enjekte eder.


-2

Freemarker sayfasında kullanıcı bilgilerini destekleme yöntemimi paylaşmak istiyorum. Her şey çok basit ve mükemmel çalışıyor!

Sadece Kimlik Doğrulama önkoşulunu yerleştirmelisiniz default-target-url(form- login'ten sonraki sayfa) Bu, bu sayfa için Denetleyici yöntemim:

@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");
}

Ve bu benim ftl kodum:

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

Ve işte bu, kullanıcı adı yetkilendirmeden sonra her sayfada görünecektir.


Yanıtlamaya çalıştığınız için teşekkür ederiz, ancak SecurityContextHolder.getContext () statik yönteminin kullanımı tam olarak kaçınmak istediğim şeydir ve bu soruyu ilk etapta sordum.
Scott Bale
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.