Spring Test & Security: Kimlik doğrulama nasıl yapılır?


124

Denetleyicilerimin URL'lerinin düzgün bir şekilde güvenli olup olmadığını nasıl test edeceğimi anlamaya çalışıyordum. Birinin bir şeyleri değiştirmesi ve yanlışlıkla güvenlik ayarlarını kaldırması durumunda.

Denetleyici yöntemim şöyle görünüyor:

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

Bunun gibi bir WebTestEnvironment kurdum:

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

Gerçek testimde şöyle bir şey yapmaya çalıştım:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

Bunu buradan aldım:

Yine de yakından bakıldığında, bu yalnızca URL'lere gerçek istekleri göndermemek, ancak yalnızca bir işlev düzeyinde hizmetleri test ederken yardımcı olur. Benim durumumda bir "erişim reddedildi" istisnası atıldı:

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

Aşağıdaki iki günlük mesajı, temelde hiçbir kullanıcının kimliğinin doğrulanmadığını ve bu ayarın Principalişe yaramadığını veya üzerine yazıldığını belirten kayda değerdir .

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS

Şirket adınız eu.ubicon, içe aktarmanızda görüntülenir. Bu bir güvenlik riski değil mi?
Kyle Bridenstine 01

2
Merhaba, yorum için teşekkürler! Yine de neden göremiyorum. Zaten açık kaynaklı bir yazılım. İlgileniyorsanız, bitbucket.org/ubicon/ubicon'a (veya en son çatal için bitbucket.org/dmir_wue/everyaware ) bakın. Bir şeyi kaçırırsam haberim olsun.
Martin Becker

Bu çözümü kontrol edin (cevap 4. bahar içindir): stackoverflow.com/questions/14308341/…
Nagy Attila

Yanıtlar:


101

Cevap aramak Aynı anda kolay ve esnek olabilecek hiçbir şey bulamadım, sonra Spring Security Referansını buldum ve mükemmele yakın çözümler olduğunu fark ettim. AOP çözümler sık test için en büyük olanlardır ve Bahar bunu sağlar @WithMockUser, @WithUserDetailsve@WithSecurityContext bu esere dair:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

Çoğu durumda, @WithUserDetails ihtiyacım olan esnekliği ve gücü bir araya getirir.

@WithUserDetails nasıl çalışır?

Temel olarak UserDetailsService, test etmek istediğiniz tüm olası kullanıcı profilleriyle bir özel oluşturmanız yeterlidir . Örneğin

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "user@company.com", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "manager@company.com", "password");
        UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_MANAGER"),
                new SimpleGrantedAuthority("PERM_FOO_READ"),
                new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                new SimpleGrantedAuthority("PERM_FOO_MANAGE")
        ));

        return new InMemoryUserDetailsManager(Arrays.asList(
                basicActiveUser, managerActiveUser
        ));
    }
}

Artık kullanıcılarımız hazır, bu nedenle bu denetleyici işlevine erişim kontrolünü test etmek istediğimizi hayal edin:

@RestController
@RequestMapping("/foo")
public class FooController {

    @Secured("ROLE_MANAGER")
    @GetMapping("/salute")
    public String saluteYourManager(@AuthenticationPrincipal User activeUser)
    {
        return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
    }
}

Burada bir var olsun eşlenen fonksiyon rota için / foo / selamıyla ve biz bir rol tabanlı güvenlik test ediyoruz @Securedtest edebilirsiniz rağmen, açıklama @PreAuthorizeve @PostAuthorizede. Biri geçerli bir kullanıcının bu selam yanıtını görüp görmediğini ve diğerinin gerçekten yasaklanmış olup olmadığını kontrol etmek için iki test oluşturalım.

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("manager@company.com")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("manager@company.com")));
    }

    @Test
    @WithUserDetails("user@company.com")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

Gördüğünüz gibi SpringSecurityWebAuxTestConfig, kullanıcılarımıza test sağlamak için ithal ettik. Her biri kendi ilgili test senaryosunda basit bir açıklama kullanarak kod ve karmaşıklığı azaltarak kullanıldı.

Daha basit Rol Tabanlı Güvenlik için @WithMockUser'ı daha iyi kullanın

Gördüğünüz gibi @WithUserDetails, uygulamalarınızın çoğu için ihtiyacınız olan tüm esnekliğe sahiptir. Rol veya izin gibi herhangi bir GrantedAuthority ile özel kullanıcıları kullanmanıza olanak tanır. Ancak sadece rollerle çalışıyorsanız, test etmek daha da kolay olabilir ve bir gelenek oluşturmaktan kaçınabilirsiniz UserDetailsService. Bu gibi durumlarda, @WithMockUser ile kullanıcı, parola ve rollerin basit bir kombinasyonunu belirtin .

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
    factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
    String value() default "user";

    String username() default "";

    String[] roles() default {"USER"};

    String password() default "password";
}

Ek açıklama, çok basit bir kullanıcı için varsayılan değerleri tanımlar. Bizim durumumuzda olduğu gibi, test ettiğimiz rota sadece kimliği doğrulanmış kullanıcının bir yönetici olmasını gerektiriyor, kullanmayı bırakıp SpringSecurityWebAuxTestConfigbunu yapabiliriz.

@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
    mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
            .accept(MediaType.ALL))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("user")));
}

Uyarı artık kullanıcı yerine manager@company.com biz sağladığı varsayılan alıyorsanız @WithMockUser: Kullanıcı ; henüz o alışkanlık ne olursa olsun biz gerçekten umurumda onun rolü, çünkü: ROLE_MANAGER.

Sonuçlar

Eğer gibi ek açıklamalarla birlikte Gördüğünüz gibi @WithUserDetailsve @WithMockUserbiz olabilir , farklı kimliği doğrulanmış kullanıcılar senaryolar arasında geçiş sadece basit testler yapmak için bizim mimarisi yabancılaşmış sınıflar için barınak yapmadan. Ayrıca @WithSecurityContext'in daha fazla esneklik için nasıl çalıştığını görmenizi de tavsiye ediyor .


Birden çok kullanıcıyla nasıl dalga geçilir ? Örneğin, ilk istek tarafından gönderilirken tom, ikincisi jerry?
ch271828n

Tom ile testinizin olduğu bir fonksiyon oluşturabilir ve aynı mantıkla başka bir test oluşturup Jerry ile test edebilirsiniz. Her test için belirli bir sonuç olacaktır, bu nedenle farklı iddialar olacaktır ve bir test başarısız olursa, hangi kullanıcının / rolün işe yaramadığını size adıyla söyleyecektir. Unutmayın ki, bir istekte kullanıcı yalnızca bir olabilir, bu nedenle bir istekte birden fazla kullanıcı belirtmenin bir anlamı yoktur.
EliuX

Üzgünüm, böyle bir örnek senaryoyu kastediyorum: Bunu test ediyoruz, tom gizli bir makale yaratıyor, sonra jerry onu okumaya çalışıyor ve Jerry onu görmemeli (çünkü bu gizli). Yani bu durumda, bu bir birim testtir ...
ch271828n

Sanki hemen hemen görünüyor BasicUserve Manager Usercevap verilen senaryo. Temel kavram, kullanıcıları önemsemek yerine aslında rollerini önemsememizdir, ancak aynı birim testinde bulunan bu testlerin her biri aslında farklı sorguları temsil eder. farklı kullanıcılar tarafından (farklı rollere sahip) aynı uç noktaya yapılır.
EliuX

61

Spring 4.0+ sürümünden bu yana, en iyi çözüm test yöntemine @WithMockUser ile açıklama eklemek

@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
    mockMvc.perform(get("/someApi"))
        .andExpect(status().isOk());
}

Projenize aşağıdaki bağımlılığı eklemeyi unutmayın

'org.springframework.security:spring-security-test:4.2.3.RELEASE'

1
Bahar harika. Teşekkürler
TuGordoBello

İyi cevap. Dahası - mockMvc kullanmanız gerekmez, ancak örneğin springframework.data'dan PagingAndSortingRepository kullanıyorsanız - yöntemleri doğrudan depodan çağırabilirsiniz (bunlar EL @PreAuthorize (......) ile açıklanmıştır)
supertramp

50

Bu ortaya çıktı SecurityContextPersistenceFilterBahar Güvenlik filtre zincirinin bir parçası olan, her zaman benim sıfırlar SecurityContext, ben çağırmadan set SecurityContextHolder.getContext().setAuthentication(principal)(veya kullanarak .principal(principal)yöntemi). Bu filtre setleri SecurityContextiçinde SecurityContextHolderbir ile SecurityContextbir den SecurityContextRepository üzerine yazarak daha önce ayarlanmış bir. Depo HttpSessionSecurityContextRepositoryvarsayılan olarak bir. HttpSessionSecurityContextRepositoryTeftiş verilen HttpRequestve erişim çalışır tekabül HttpSession. Varsa, ' SecurityContextdan okumaya çalışacaktır HttpSession. Bu başarısız olursa, arşiv boş birSecurityContext .

Bu nedenle, benim çözümüm HttpSession, aşağıdakileri tutan istekle birlikte iletmektir SecurityContext:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class Test extends WebappTestEnvironment {

    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        MockHttpSession session = new MockHttpSession();
        session.setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                new MockSecurityContext(principal));


        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
                    .session(session))
            .andExpect(status().isOk());
    }
}


Bir Authentication nesnesi oluşturmanın ve buna karşılık gelen öznitelikle bir oturum eklemenin o kadar da kötü olduğunu düşünmüyorum. Bunun geçerli bir "geçici çözüm" olduğunu düşünüyor musunuz? Öte yandan doğrudan destek elbette harika olurdu. Oldukça temiz görünüyor. Bağlantı için teşekkürler!
Martin Becker

harika çözüm. benim için çalıştı! getPrincipal()Bana göre biraz yanıltıcı olan korunan yöntemin isimlendirilmesiyle ilgili küçük bir sorun . ideal olarak adlandırılmış olmalıydı getAuthentication(). benzer şekilde, signedIn()testinizde yerel değişkenauthauthenticationprincipal
Tanvir

"GetPrincipal (" test1 ") nedir ¿?? Bunun nerede olduğunu açıklayabilir misiniz? Şimdiden teşekkürler
user2992476

@ user2992476 Muhtemelen UsernamePasswordAuthenticationToken türünde bir nesne döndürür. Alternatif olarak, GrantedAuthority oluşturursunuz ve bu nesneyi oluşturursunuz.
bluelurker

31

Pom.xml dosyasına ekleyin:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.0.RC2</version>
    </dependency>

ve org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessorsyetkilendirme talebi için kullanın . Örnek kullanıma https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ) adresinden bakın .

Güncelleme:

4.0.0.RC2, yay güvenliği 3.x için çalışır. Yay güvenliği için 4 yay güvenlik testi, yay güvenliğinin bir parçası haline gelir ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , sürüm aynıdır ).

Kurulum değiştirildi: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())  
            .build();
}

Temel kimlik doğrulama örneği: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .


Bu aynı zamanda bir giriş güvenlik filtresiyle giriş yapmaya çalışırken 404 alma sorunumu da çözdü. Teşekkürler!
Ian Newland

Merhaba, GKislin tarafından belirtildiği gibi test ederken. "Kimlik doğrulama başarısız oldu UserDetailsService null döndürdü, bu bir arabirim sözleşmesi ihlali" hatasını alıyorum. Herhangi bir öneri lütfen. final AuthenticationRequest auth = new AuthenticationRequest (); auth.setUsername (kullanıcı kimliği); auth.setPassword (şifre); mockMvc.perform (.. sonrası ( "/ API / kimlik doğrulama /"), içerik (json (yetkilendirme)) contentType (MediaType.APPLICATION_JSON));
Sanjeev

7

İşte Base64 temel kimlik doğrulamasını kullanarak Spring MockMvc Güvenlik Yapılandırmasını Test etmek isteyenler için bir örnek.

String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

Maven Bağımlılığı

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>

3

Kısa cevap:

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
    final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
            .defaultRequest(defaultRequestBuilder)
            .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
            .apply(springSecurity(springSecurityFilterChain))
            .build();
}

private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                             final MockHttpServletRequest request) {
    requestBuilder.session((MockHttpSession) request.getSession());
    return request;
}

formLoginBahar güvenlik testinden sonra , isteklerinizin her biri otomatik olarak oturum açmış kullanıcı olarak çağrılacaktır.

Uzun cevap:

Bu çözümü kontrol edin (yanıt 4. bahar içindir): Spring 3.2 yeni mvc testi ile bir kullanıcı nasıl oturum açılır?


2

Testlerde SecurityContextHolder'ı kullanmaktan kaçınmak için seçenekler:

  • Seçenek 1 : Taklitler kullanın - SecurityContextHolderBazı sahte kitaplıklar kullanarak sahte demek istiyorum - EasyMock örneğin
  • Seçenek 2 : SecurityContextHolder.get...Bazı hizmetlerde kodunuzda aramayı sarın - örneğin, arabirimi uygulayan SecurityServiceImplyöntemde ve ardından testlerinizde, bu arabirimin, erişim olmadan istenen sorumlusu döndüren sahte uygulamasını basitçe oluşturabilirsiniz .getCurrentPrincipalSecurityServiceSecurityContextHolder

Belki de bütün resmi anlamıyorum. Benim sorunum, SecurityContextPersistenceFilter'in bir HttpSessionSecurityContextRepository'den bir SecurityContext kullanarak SecurityContext'i değiştirmesiydi, bu da karşılık gelen HttpSession'dan SecurityContext'i okur. Böylece oturumu kullanarak çözüm. SecurityContextHolder'a yapılan çağrı ile ilgili olarak: Cevabımı artık SecurityContextHolder'a bir çağrı kullanmayacağım şekilde düzenledim. Ama aynı zamanda herhangi bir sarma veya fazladan alay eden kitaplıklar sunmadan. Bunun daha iyi bir çözüm olduğunu düşünüyor musunuz?
Martin Becker

Maalesef tam olarak ne aradığınızı anlamadım ve bulduğunuz çözümden daha iyi bir cevap veremiyorum ve - bu iyi bir seçenek gibi görünüyor.
Pavla Nováková

Pekala, teşekkürler. Teklifimi şimdilik çözüm olarak kabul edeceğim.
Martin Becker
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.