Uji Musim Semi & Keamanan: Bagaimana cara meniru otentikasi?

124

Saya mencoba mencari cara untuk menguji unit jika URL pengontrol saya diamankan dengan benar. Untuk berjaga-jaga jika seseorang mengubah sesuatu dan secara tidak sengaja menghapus pengaturan keamanan.

Metode pengontrol saya terlihat seperti ini:

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

Saya menyiapkan WebTestEnvironment seperti ini:

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

Dalam tes saya yang sebenarnya, saya mencoba melakukan sesuatu seperti ini:

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

}

Saya mengambil ini di sini:

Namun jika seseorang melihat lebih dekat, ini hanya membantu saat tidak mengirimkan permintaan sebenarnya ke URL, tetapi hanya saat menguji layanan pada tingkat fungsi. Dalam kasus saya, pengecualian "akses ditolak" muncul:

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]
        ...

Dua pesan log berikut pada dasarnya patut diperhatikan mengatakan bahwa tidak ada pengguna yang diautentikasi yang menunjukkan bahwa pengaturan Principaltidak berfungsi, atau telah ditimpa.

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
Martin Becker
sumber
Nama perusahaan Anda, eu.ubicon, ditampilkan di impor Anda. Bukankah itu risiko keamanan?
Kyle Bridenstine
2
Hai, terima kasih atas komentarnya! Tapi aku tidak mengerti kenapa. Ini adalah perangkat lunak sumber terbuka. Jika Anda tertarik, lihat bitbucket.org/ubicon/ubicon (atau bitbucket.org/dmir_wue/everyaware untuk fork terbaru). Beri tahu saya jika saya melewatkan sesuatu.
Martin Becker
Periksa solusi ini (jawabannya untuk musim semi 4): stackoverflow.com/questions/14308341/…
Nagy Attila

Jawaban:

101

Mencari jawaban Saya tidak dapat menemukan jawaban yang mudah dan fleksibel pada saat yang sama, kemudian saya menemukan Referensi Keamanan Musim Semi dan saya menyadari ada solusi yang hampir sempurna. Solusi AOP sering kali merupakan solusi terbaik untuk pengujian, dan Spring menyediakannya @WithMockUser, @WithUserDetailsdan @WithSecurityContext, dalam artefak ini:

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

Dalam kebanyakan kasus, @WithUserDetailskumpulkan fleksibilitas dan kekuatan yang saya butuhkan.

Bagaimana cara kerja @WithUserDetails?

Pada dasarnya Anda hanya perlu membuat kustom UserDetailsServicedengan semua kemungkinan profil pengguna yang ingin Anda uji. Misalnya

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "[email protected]", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "[email protected]", "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
        ));
    }
}

Sekarang kita memiliki pengguna yang siap, jadi bayangkan kita ingin menguji kontrol akses ke fungsi pengontrol ini:

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

Di sini kami memiliki fungsi yang dipetakan ke rute / foo / salute dan kami menguji keamanan berbasis peran dengan @Securedanotasi, meskipun Anda dapat menguji @PreAuthorizedan @PostAuthorizejuga. Mari buat dua pengujian, satu untuk memeriksa apakah pengguna yang valid dapat melihat respons hormat ini dan yang lainnya untuk memeriksa apakah itu benar-benar dilarang.

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

    @Autowired
    private MockMvc mockMvc;

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

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

Seperti yang Anda lihat, kami mengimpor SpringSecurityWebAuxTestConfiguntuk memberikan pengujian kepada pengguna kami. Masing-masing digunakan pada kasus uji yang sesuai hanya dengan menggunakan anotasi langsung, mengurangi kode dan kompleksitas.

Lebih baik gunakan @WithMockUser untuk Keamanan Berbasis Peran yang lebih sederhana

Seperti yang Anda lihat, Anda @WithUserDetailsmemiliki semua fleksibilitas yang Anda butuhkan untuk sebagian besar aplikasi Anda. Ini memungkinkan Anda untuk menggunakan pengguna khusus dengan GrantedAuthority apa pun, seperti peran atau izin. Namun jika Anda hanya bekerja dengan peran, pengujian bisa jadi lebih mudah dan Anda dapat menghindari pembuatan kustom UserDetailsService. Dalam kasus seperti itu, tentukan kombinasi sederhana antara pengguna, sandi, dan peran dengan @WithMockUser .

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

Anotasi menentukan nilai default untuk pengguna yang sangat dasar. Seperti dalam kasus kami, rute yang kami uji hanya mengharuskan pengguna yang diautentikasi menjadi manajer, kami dapat berhenti menggunakan SpringSecurityWebAuxTestConfigdan melakukan ini.

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

Perhatikan bahwa sekarang alih-alih manajer [email protected] kita mendapatkan default yang disediakan oleh @WithMockUser: pengguna ; namun tidak akan peduli karena apa yang kita benar-benar peduli adalah perannya: ROLE_MANAGER.

Kesimpulan

Seperti yang Anda lihat dengan anotasi like @WithUserDetailsdan @WithMockUserkita dapat beralih di antara skenario pengguna terautentikasi yang berbeda tanpa membangun kelas yang terasing dari arsitektur kita hanya untuk melakukan pengujian sederhana. Ini juga merekomendasikan Anda untuk melihat bagaimana @WithSecurityContext bekerja untuk lebih banyak fleksibilitas.

EliuX
sumber
Bagaimana cara mengejek banyak pengguna ? Misalnya, permintaan pertama dikirim oleh tom, sedangkan yang kedua oleh jerry?
ch271828n
Anda dapat membuat fungsi di mana pengujian Anda dengan tom dan membuat pengujian lain dengan logika yang sama dan mengujinya dengan Jerry. Akan ada hasil tertentu untuk setiap pengujian sehingga akan ada pernyataan yang berbeda dan jika pengujian gagal, Anda akan diberi tahu dengan namanya pengguna / peran mana yang tidak berfungsi. Ingatlah bahwa dalam sebuah permintaan, pengguna hanya bisa satu, jadi menentukan beberapa pengguna dalam sebuah permintaan tidak masuk akal.
EliuX
Maaf maksud saya skenario contoh seperti itu: Kami mengujinya, tom membuat artikel rahasia, lalu jerry mencoba membacanya, dan jerry seharusnya tidak melihatnya (karena itu rahasia). Jadi dalam kasus ini, ini adalah tes satu unit ...
ch271828n
Sepertinya skenario BasicUserdan Manager Userdiberikan dalam jawaban. Konsep utamanya adalah bahwa alih-alih memedulikan pengguna, kami sebenarnya peduli dengan peran mereka, tetapi masing-masing pengujian tersebut, yang terletak di pengujian unit yang sama, sebenarnya mewakili kueri yang berbeda. dilakukan oleh pengguna yang berbeda (dengan peran berbeda) ke titik akhir yang sama.
EliuX
61

Sejak Spring 4.0+, solusi terbaik adalah memberi anotasi metode pengujian dengan @WithMockUser

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

Ingatlah untuk menambahkan ketergantungan berikut ke proyek Anda

'org.springframework.security:spring-security-test:4.2.3.RELEASE'
GummyBear21
sumber
1
Musim semi itu luar biasa. Terima kasih
TuGordoBello
Jawaban yang bagus. Selain itu - Anda tidak perlu menggunakan mockMvc, tetapi jika Anda menggunakan misalnya PagingAndSortingRepository dari springframework.data - Anda cukup memanggil metode dari repositori secara langsung (yang dianotasi dengan EL @PreAuthorize (......))
supertramp
50

Ternyata SecurityContextPersistenceFilter, yang merupakan bagian dari rantai filter Keamanan Musim Semi, selalu me-reset my SecurityContext, yang saya atur panggilan SecurityContextHolder.getContext().setAuthentication(principal)(atau dengan menggunakan .principal(principal)metode). Filter ini menetapkan SecurityContextdalam SecurityContextHolderdengan SecurityContextdari SecurityContextRepository Timpa yang saya set sebelumnya. Repositori adalah secara HttpSessionSecurityContextRepositorydefault. The HttpSessionSecurityContextRepositorymemeriksa yang diberikan HttpRequestdan mencoba untuk mengakses yang sesuai HttpSession. Jika ada, itu akan mencoba membaca SecurityContextdari HttpSession. Jika ini gagal, repositori menghasilkan kosong SecurityContext.

Jadi, solusi saya adalah meneruskan HttpSessionpermintaan, yang menyimpan 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());
    }
}
Martin Becker
sumber
2
Kami belum menambahkan dukungan resmi untuk Keamanan Musim Semi. Lihat jira.springsource.org/browse/SEC-2015 Garis besar tentang tampilannya ditentukan di github.com/SpringSource/spring-test-mvc/blob/master/src/test/…
Rob Winch
Saya tidak berpikir membuat objek Authentication dan menambahkan sesi dengan atribut yang sesuai itu buruk. Apakah menurut Anda ini adalah "solusi" yang valid? Dukungan langsung di sisi lain tentu saja akan sangat bagus. Terlihat cukup rapi. Terima kasih untuk tautannya!
Martin Becker
solusi yang bagus. bekerja untuk saya! hanya masalah kecil dengan penamaan metode getPrincipal()yang dilindungi yang menurut saya agak menyesatkan. idealnya itu harus diberi nama getAuthentication(). demikian pula, dalam signedIn()pengujian Anda , variabel lokal harus dinamai authatau authenticationsebagai penggantiprincipal
Tanvir
Apa itu "getPrincipal (" test1 ") ¿?? Bisakah Anda menjelaskan di mana itu? Terima kasih sebelumnya
user2992476
@ user2992476 Ini mungkin mengembalikan objek jenis UsernamePasswordAuthenticationToken. Alternatifnya, Anda membuat GrantedAuthority dan membuat objek ini.
bluelurker
31

Tambahkan pom.xml:

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

dan digunakan org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessorsuntuk permintaan otorisasi. Lihat penggunaan sampel di https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).

Memperbarui:

4.0.0.RC2 berfungsi untuk keamanan pegas 3.x. Untuk keamanan pegas, uji keamanan pegas 4 menjadi bagian dari keamanan pegas ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , versinya sama ).

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

Contoh untuk otentikasi dasar: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .

Grigory Kislin
sumber
Ini juga memperbaiki masalah saya dengan mendapatkan 404 ketika mencoba masuk melalui filter keamanan masuk. Terima kasih!
Ian Newland
Hai, saat menguji seperti yang disebutkan oleh GKislin. Saya mendapatkan kesalahan berikut "Otentikasi gagal UserDetailsService mengembalikan null, yang merupakan pelanggaran kontrak antarmuka". Ada saran. akhir AuthenticationRequest auth = new AuthenticationRequest (); auth.setUsername (userId); auth.setPassword (kata sandi); mockMvc.perform (post ("/ api / auth /"). content (json (auth)). contentType (MediaType.APPLICATION_JSON));
Sanjeev
7

Berikut adalah contoh bagi mereka yang ingin Menguji Konfigurasi Keamanan MockMvc Spring menggunakan otentikasi dasar Base64.

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

Ketergantungan Maven

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>
Jay
sumber
3

Jawaban singkat:

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

Setelah melakukan formLoginuji keamanan musim semi, setiap permintaan Anda akan secara otomatis dipanggil sebagai pengguna yang masuk.

Jawaban panjang:

Periksa solusi ini (jawabannya untuk pegas 4): Cara login pengguna dengan pengujian mvc baru pegas 3.2

Nagy Attila
sumber
2

Opsi untuk menghindari penggunaan SecurityContextHolder dalam pengujian:

  • Opsi 1 : gunakan tiruan - Maksud saya mengejek SecurityContextHoldermenggunakan beberapa perpustakaan tiruan - EasyMock misalnya
  • Opsi 2 : bungkus panggilan SecurityContextHolder.get...dalam kode Anda di beberapa layanan - misalnya SecurityServiceImpldengan metode getCurrentPrincipalyang mengimplementasikan SecurityServiceantarmuka dan kemudian dalam pengujian Anda, Anda cukup membuat implementasi tiruan dari antarmuka ini yang mengembalikan prinsip yang diinginkan tanpa akses SecurityContextHolder.
Pavla Nováková
sumber
Mh, mungkin saya tidak mengerti secara keseluruhan. Masalah saya adalah bahwa SecurityContextPersistenceFilter menggantikan SecurityContext menggunakan SecurityContext dari HttpSessionSecurityContextRepository, yang pada gilirannya membaca SecurityContext dari HttpSession yang sesuai. Demikian solusinya menggunakan sesi. Mengenai panggilan ke SecurityContextHolder: Saya mengedit jawaban saya sehingga saya tidak menggunakan panggilan ke SecurityContextHolder lagi. Tetapi juga tanpa memperkenalkan pustaka pembungkus atau tambahan yang mengejek. Apakah menurut Anda, ini adalah solusi yang lebih baik?
Martin Becker
Maaf, saya tidak mengerti persis apa yang Anda cari dan saya tidak dapat memberikan jawaban yang lebih baik daripada solusi yang Anda buat dan - tampaknya ini adalah pilihan yang baik.
Pavla Nováková
Baiklah terima kasih. Saya akan menerima proposal saya sebagai solusi untuk saat ini.
Martin Becker