Spring Security ile birim testi


140

Şirketim, Spring MVC'yi bir sonraki projelerimizden birinde kullanmamız gerekip gerekmediğini belirlemek için değerlendiriyor. Şimdiye kadar gördüklerimi seviyorum ve şu anda Spring Security modülüne bakıp kullanabileceğimiz / kullanmamız gereken bir şey olup olmadığına karar veriyorum.

Güvenlik gereksinimlerimiz oldukça basit; bir kullanıcının sadece sitenin belirli bölümlerine erişebilmesi için bir kullanıcı adı ve şifre verebilmesi gerekir (hesapları hakkında bilgi almak gibi); sitede anonim bir kullanıcıya erişim verilmesi gereken birkaç sayfa (SSS, Destek vb.) vardır.

Oluşturduğum prototipte, kimliği doğrulanmış bir kullanıcı için Oturumda bir "LoginCredentials" nesnesi (sadece kullanıcı adı ve şifre içeriyor) saklıyorum; bazı denetleyiciler, oturum açmış olan kullanıcı adına bir başvuru almak için bu nesnenin oturumda olup olmadığını kontrol eder. Bunun yerine evde yetiştirilen bu mantığı Spring Security ile değiştirmek istiyorum, bu da "giriş yapan kullanıcıları nasıl izleyebiliriz?" ve "kullanıcıların kimliğini nasıl doğrularız?" denetleyicim / işletme kodumdan.

Görünüşe göre Spring Security, uygulamanızın herhangi bir yerinden kullanıcı adı / ana bilgilere erişebilmek için (iş parçacığı başına) bir "bağlam" nesnesi sağlıyor ...

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

... bu nesnenin bir bakıma (küresel) bir singleton olduğu gibi çok Baharsız görünüyor.

Benim sorum şudur: Spring Security'de kimliği doğrulanmış kullanıcı hakkındaki bilgilere erişmenin standart yolu bu ise, Birim Testleri, Doğrulanmış kullanıcı?

Bunu her test vakasının başlatma yönteminde kablolamam gerekir mi?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

Bu aşırı ayrıntılı görünüyor. Daha kolay bir yol var mı?

SecurityContextHolderNesne kendisi çok un-Bahar-gibi görünüyor ...

Yanıtlar:


48

Sorun, Spring Security'nin Kimlik Doğrulama nesnesini kapta bir fasulye olarak kullanıma sunmamasıdır, bu nedenle kutudan kolayca enjekte etmenin veya otomatik olarak bağlamanın bir yolu yoktur.

Spring Security kullanmaya başlamadan önce, Anapara'yı saklamak için kapta oturum kapsamlı bir fasulye oluşturacağız, bunu bir "AuthenticationService" (singleton) içine enjekte edecek ve daha sonra bu fasulyeyi mevcut Anapara hakkında bilgi gerektiren diğer hizmetlere enjekte edeceğiz.

Kendi kimlik doğrulama hizmetinizi uyguluyorsanız, temel olarak aynı şeyi yapabilirsiniz: "temel" özellikli bir oturum kapsamı çekirdeği oluşturun, bunu kimlik doğrulama hizmetinize enjekte edin, kimlik doğrulama hizmetinin özelliği başarılı bir kimlik doğrulamasına ayarlamasını sağlayın ve ardından auth hizmetini ihtiyacınız olan diğer fasulye için kullanılabilir hale getirin.

SecurityContextHolder kullanma konusunda çok kötü hissetmezdim. rağmen. Statik / Singleton olduğunu ve Spring'in bu tür şeyleri kullanmayı reddettiğini biliyorum, ancak uygulamalarının ortama bağlı olarak uygun şekilde davranmaya özen gösterdiği: Bir Servlet kapsayıcısında oturum kapsamı, bir JUnit testinde iplik kapsamı vb. Gerçek sınırlayıcı faktör bir Singleton, farklı ortamlara esnek olmayan bir uygulama sağladığı zamandır.


Teşekkürler, bu yararlı bir tavsiye. Şimdiye kadar yaptığım temelde SecurityContextHolder.getContext () (kendi kendime birkaç sarmalayıcı yöntem aracılığıyla), bu yüzden en azından sadece bir sınıftan çağrılması ile devam etmektir.
matt b

2
Her ne kadar sadece bir not olsa da - ServletContextHolder'ın HttpSession veya bir web sunucusu ortamında çalışıp çalışmadığını bilmenin bir yolu olduğunu düşünmüyorum - başka bir şey kullanacak şekilde yapılandırmadıkça ThreadLocal kullanıyor (yalnızca diğer iki yerleşik mod devralınabiliyor) ve Global)
matt b

İlkbaharda oturum / istek kapsamındaki fasulye kullanmanın tek dezavantajı, bir JUnit testinde başarısız olmalarıdır. Yapabileceğiniz şey, varsa oturum / istek kullanacak ve iş parçacığına geri dönecek özel bir kapsam uygulamaktır. Benim tahminim Spring Security benzer bir şey yapıyor ...
cliff.meyers

Amacım, oturumlar olmadan bir Rest api oluşturmak. Belki de yenilenebilir bir jetonla. Bu benim soruma cevap vermese de yardımcı oldu. Teşekkürler
Pomagranite

166

Sadece normal şekilde yapın ve ardından SecurityContextHolder.setContext()test sınıfınızda kullanarak ekleyin , örneğin:

Denetleyici:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

Ölçek:

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);

2
@Leonardo bu kontrolöre nereye eklenmelidir Authentication a? Her yöntem çağrısında anlayabildiğim gibi? Enjekte etmek yerine "yay" ın eklenmesi uygun mudur?
Oleg Kuts

Ancak TestNG ile çalışmayacağını unutmayın, çünkü SecurityContextHolder yerel iş parçacığı değişkenini tutar, böylece bu değişkeni testler arasında paylaşırsınız ...
asukasz Woźniczka

Do it @BeforeEach(JUnit5) ya da @Before(JUnit 4). İyi ve basit.
WesternGun

30

Kimlik Doğrulama nesnelerinin nasıl oluşturulacağı ve enjekte edileceği sorusunu yanıtlamadan Spring Security 4.0, test söz konusu olduğunda bazı hoş geldiniz alternatifleri sunar. @WithMockUserEk açıklama düzgün bir şekilde (isteğe bağlı makamlar, adınızı, şifrenizi ve roller) sahte kullanıcıyı belirtmek için geliştirici sağlar:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

Kullanımına seçeneği de vardır @WithUserDetailsbir taklit UserDetailsdöndü UserDetailsService, örneğin

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

Daha fazla ayrıntı , Spring Security başvuru belgelerindeki @WithMockUser ve @WithUserDetails bölümlerinde bulunabilir (yukarıdaki örneklerin kopyalandığı)


29

Endişelenmeye oldukça haklısınız - statik yöntem çağrıları, bağımlılıklarınızı kolayca alamayacağınız için birim testi için özellikle sorunludur. Size göstereceğim şey, Spring IoC konteynerinin sizin için kirli işi yapmasına nasıl izin vereceğinizi ve sizi düzgün, test edilebilir bir kodla bırakmanızı sağlayacak. SecurityContextHolder bir çerçeve sınıfıdır ve düşük seviyeli güvenlik kodunuzun ona bağlanması uygun olsa da, muhtemelen UI bileşenlerinize (yani denetleyiciler) daha temiz bir arayüz göstermek istiyorsunuz.

cliff.meyers bunun bir yolundan bahsetti - kendi "ana" türünüzü yaratın ve tüketicilere bir örnek enjekte edin. 2.x'de tanıtılan Spring < aop: scoped-proxy /> etiketi, istek kapsamı fasulye tanımıyla birleştirildi ve fabrika yöntemi desteği, en okunabilir koda bilet olabilir.

Aşağıdaki gibi çalışabilir:

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

Şimdiye kadar karmaşık bir şey yok, değil mi? Aslında muhtemelen bunların çoğunu zaten yapmak zorundaydınız. Ardından, fasulye bağlamınızda, prensibi tutmak için istek kapsamındaki bir fasulye tanımlayın:

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
    <aop:scoped-proxy/>
</bean>

<bean id="controller" class="MyUserAwareController">
    <property name="currentUser" ref="userDetails"/>
    <!-- other props -->
</bean>

Aop: scoped-proxy etiketinin büyüsü sayesinde, her yeni HTTP isteği geldiğinde ve currentUser özelliğine yapılan tüm başvurular doğru bir şekilde çözüldüğünde getUserDetails statik yöntemi çağrılır. Artık birim testleri önemsiz hale geliyor:

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

Bu yardımcı olur umarım!


9

Şahsen Powermock'u Mockito veya Easymock ile birlikte birim / entegrasyon testinde statik SecurityContextHolder.getSecurityContext () ile alay etmek için kullanırım.

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

Kuşkusuz burada oldukça fazla kazan plakası kodu var, yani bir Kimlik Doğrulama nesnesini alay et, Kimlik Doğrulamayı döndürmek için bir SecurityContext'i alay et ve son olarak SecurityContextHolder'ı alay et. vb. (test dışı) kodunuzu değiştirmek zorunda kalmadan


7

Bu durumda statik kullanmak güvenli kod yazmanın en iyi yoludur.

Evet, statikler genellikle kötüdür - genellikle, ancak bu durumda statik, istediğiniz şeydir. 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ığındaki 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 saldırmak için daha fazla 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ıf yoluna kaydırabilecek yapılandırmayı geçersiz kılmak için bir yola ihtiyaçları var. 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.


4

Aynı soruyu burada kendime sordum ve yakın zamanda bulduğum bir cevap gönderdim. Kısa cevap: a enjekte edin SecurityContextve SecurityContextHoldersadece Spring konfigürasyonunuzdaSecurityContext


3

Genel

Bu arada (3.2 sürümünden beri, 2013 yılında, SEC-2298 sayesinde ) kimlik doğrulama @AuthenticationPrincipal ek açıklaması kullanılarak MVC yöntemlerine enjekte edilebilir :

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

Testler

Birim testinizde açıkça bu Yöntemi çağırabilirsiniz. Kullanarak yapılan entegrasyon testlerinde , kullanıcıyı şu şekilde enjekte etmek için org.springframework.test.web.servlet.MockMvckullanabilirsiniz org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user():

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

Ancak bu doğrudan SecurityContext'i dolduracaktır. Kullanıcının testinizdeki bir oturumdan yüklendiğinden emin olmak istiyorsanız, bunu kullanabilirsiniz:

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
    return new RequestPostProcessor() {
        @Override
        public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
            final SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
            );
            request.getSession().setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
            );
            return request;
        }
    };
}

2

Spring'in soyut test sınıflarına ve burada konuşulan sahte nesnelere bir göz atarım . Ünite ve entegrasyon testlerini kolaylaştırmak için Spring yönetilen nesnelerinizi otomatik olarak kablolamanın güçlü bir yolunu sunarlar.


Bu test sınıfları yardımcı olsa da, burada uygulanıp uygulanmadığından emin değilim. Testlerimin ApplicationContext ile ilgili bir konsepti yoktur - bunlara ihtiyaç duymazlar. Tüm ihtiyacım test yöntemi çalıştırmadan önce SecurityContext doldurulduğundan emin olmaktır - sadece bir ThreadLocal ilk ayarlamak zorunda kirli hissediyor
matt b

1

Kimlik doğrulama, sunucu ortamındaki bir iş parçacığının işletim sistemindeki bir özelliğiyle aynıdır. Kimlik doğrulama bilgilerine erişmek için bir fasulye örneğine sahip olmak, herhangi bir fayda olmadan uygun olmayan yapılandırma ve kablolama yükü olacaktır.

Test kimlik doğrulaması ile ilgili olarak hayatınızı nasıl kolaylaştırabileceğinizin birkaç yolu vardır. Benim favorim, özel bir açıklama yapmak @Authenticatedve onu yöneten yürütme dinleyicisini test etmektir . DirtiesContextTestExecutionListenerİlham olup olmadığını kontrol edin .


0

Oldukça fazla çalışmadan sonra istenen davranışı yeniden üretebildim. MockMvc üzerinden giriş taklit vardı. Çoğu birim testi için çok ağırdır, ancak entegrasyon testleri için yararlıdır.

Tabii ki Spring Security 4.0'da testlerimizi kolaylaştıracak bu yeni özellikleri görmeye hazırım.

package [myPackage]

import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.servlet.http.HttpSession;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{

    private MockMvc mockMvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private MockHttpServletRequest request;

    @Autowired
    private WebApplicationContext webappContext;

    @Before  
    public void init() {  
        mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
    }  


    @Test
    public void testTwoReads() throws Exception{                        

    HttpSession session  = mockMvc.perform(post("/j_spring_security_check")
                        .param("j_username", "admin_001")
                        .param("j_password", "secret007"))
                        .andDo(print())
                        .andExpect(status().isMovedTemporarily())
                        .andExpect(redirectedUrl("/index"))
                        .andReturn()
                        .getRequest()
                        .getSession();

    request.setSession(session);

    SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

    SecurityContextHolder.setContext(securityContext);

        // Your test goes here. User is logged with 
}
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.