Cara mengatur pengguna yang diautentikasi secara manual di Spring Security / SpringMVC

107

Setelah pengguna baru mengirimkan formulir 'Akun baru', saya ingin memasukkan pengguna tersebut secara manual sehingga mereka tidak perlu masuk di halaman berikutnya.

Halaman login form normal melalui pencegat keamanan pegas berfungsi dengan baik.

Di pengontrol formulir akun baru, saya membuat UsernamePasswordAuthenticationToken dan menyetelnya di SecurityContext secara manual:

SecurityContextHolder.getContext().setAuthentication(authentication);

Pada halaman yang sama saya kemudian memeriksa apakah pengguna login dengan:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Ini mengembalikan otoritas yang saya tetapkan sebelumnya di otentikasi. Semuanya baik-baik saja.

Tetapi ketika kode yang sama ini dipanggil di halaman berikutnya yang saya muat, token otentikasi hanya UserAnonymous.

Saya tidak jelas mengapa itu tidak menyimpan otentikasi yang saya setel pada permintaan sebelumnya. Ada pemikiran?

  • Mungkinkah ada hubungannya dengan ID sesi yang tidak disiapkan dengan benar?
  • Apakah ada sesuatu yang mungkin menimpa otentikasi saya?
  • Mungkin saya hanya perlu langkah lain untuk menyimpan otentikasi?
  • Atau adakah sesuatu yang perlu saya lakukan untuk mendeklarasikan otentikasi di seluruh sesi daripada satu permintaan?

Hanya mencari beberapa pemikiran yang mungkin dapat membantu saya melihat apa yang terjadi di sini.

David Parks
sumber
1
Anda dapat mengikuti jawaban saya ke stackoverflow.com/questions/4824395/…
AlexK
2
Pembaca, berhati-hatilah dari jawaban untuk pertanyaan ini jika mereka memberitahu Anda lakukan: SecurityContextHolder.getContext().setAuthentication(authentication). Ini berfungsi, dan umum, tetapi ada kekurangan fungsionalitas serius yang akan Anda temui jika Anda melakukannya. Untuk info lebih lanjut, lihat pertanyaan saya, dan jawabannya: stackoverflow.com/questions/47233187/…
kambing

Jawaban:

62

Saya memiliki masalah yang sama seperti Anda beberapa waktu lalu. Saya tidak dapat mengingat detailnya tetapi kode berikut membuat semuanya berfungsi untuk saya. Kode ini digunakan dalam aliran Spring Webflow, oleh karena itu adalah kelas RequestContext dan ExternalContext. Tetapi bagian yang paling relevan bagi Anda adalah metode doAutoLogin.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}
Kevin Stembridge
sumber
2
Terima kasih, kode ini sangat membantu dalam membantu saya mengetahui bahwa saya memecahkan masalah di area yang tepat. Sepertinya saya memiliki senjata merokok, ini membuat id sesi baru setelah otentikasi manual, tetapi id sesi lama masih diidentifikasi dari cookie. Aku harus mencari tahu kenapa sekarang, tapi setidaknya aku jelas berada di jalur yang benar. Terima kasih!
David Parks
4
Siapa pun yang mengikuti panduan ini juga harus melihat masalah terkait ini: stackoverflow.com/questions/4824395/…
David Parks
14
Bisakah Anda menjelaskan bahwa bagaimana Anda mendapatkan authenticationProvider
Vivex
1
@ s1moner3d Anda harus dapat menyuntikkannya melalui IoC
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi
66

Saya tidak dapat menemukan solusi lengkap lainnya jadi saya pikir saya akan memposting milik saya. Ini mungkin sedikit peretasan, tetapi ini menyelesaikan masalah untuk masalah di atas:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
Stuart McIntyre
sumber
7
+1 - Ini membantu saya! Saya melewatkan pembaruan SPRING_SECURITY_CONTEXT. ... Tapi seberapa "kotor" ini?
l3dx
12
mana Anda mendapatkan authenticationManagerdari?
Isaac
2
authenticationManager diautowired di kelas Anda seperti @Autowired AuthenticationServiceImpl authenticationManager ini. Dan juga harus ada injeksi kacang dalam konfigurasi xml Anda, jadi Spring tahu apa yang harus disuntikkan.
1
di mana implementasi AuthenticationServiceImpl? Apa yang dimiliki kelas ini?
Pra_A
3
Mengapa perlu membuat sesi baru? Apakah SecurityContext tidak menanganinya?
Vlad Manuel Mureșan
17

Akhirnya menemukan akar masalahnya.

Ketika saya membuat konteks keamanan secara manual, tidak ada objek sesi yang dibuat. Hanya ketika permintaan selesai diproses mekanisme Keamanan Musim Semi menyadari bahwa objek sesi null (ketika mencoba untuk menyimpan konteks keamanan ke sesi setelah permintaan telah diproses).

Di akhir permintaan, Spring Security membuat objek sesi dan ID sesi baru. Namun ID sesi baru ini tidak pernah berhasil masuk ke browser karena ini terjadi di akhir permintaan, setelah respons ke browser dibuat. Hal ini menyebabkan ID sesi baru (dan karenanya konteks Keamanan yang berisi pengguna yang saya masuk secara manual) hilang ketika permintaan berikutnya berisi ID sesi sebelumnya.

David Parks
sumber
4
Sejujurnya ini terasa seperti cacat desain pada keamanan musim semi lebih dari apa pun. Ada banyak kerangka kerja yang ditulis dalam bahasa lain yang tidak akan bermasalah dengan ini, tetapi Keamanan Musim Semi rusak begitu saja.
chubbsondubs
3
dan solusinya adalah?
s1moner3d
2
dan apa solusinya?
Thiago
6

Aktifkan logging debug untuk mendapatkan gambaran yang lebih baik tentang apa yang sedang terjadi.

Anda dapat mengetahui apakah cookie sesi disetel dengan menggunakan debugger sisi browser untuk melihat header yang dikembalikan dalam respons HTTP. (Ada cara lain juga.)

Salah satu kemungkinannya adalah SpringSecurity menyetel cookie sesi aman, dan halaman Anda berikutnya yang diminta memiliki URL "http", bukan URL "https". (Browser tidak akan mengirimkan cookie aman untuk URL "http".)

Stephen C
sumber
Terima kasih, ini semua adalah saran yang sangat membantu dan relevan!
David Parks
5

Fitur pemfilteran baru di Servlet 2.4 pada dasarnya mengurangi batasan bahwa filter hanya dapat beroperasi dalam aliran permintaan sebelum dan sesudah pemrosesan permintaan yang sebenarnya oleh server aplikasi. Sebaliknya, filter Servlet 2.4 sekarang dapat berinteraksi dengan operator permintaan di setiap titik pengiriman. Ini berarti bahwa ketika sumber daya Web meneruskan permintaan ke sumber daya lain (misalnya, servlet meneruskan permintaan ke halaman JSP dalam aplikasi yang sama), filter dapat beroperasi sebelum permintaan ditangani oleh sumber daya yang ditargetkan. Ini juga berarti bahwa jika sumber daya Web menyertakan keluaran atau fungsi dari sumber daya Web lain (misalnya, halaman JSP yang menyertakan keluaran dari beberapa halaman JSP lainnya), filter Servlet 2.4 dapat bekerja sebelum dan sesudah setiap sumber daya yang disertakan. .

Untuk mengaktifkan fitur itu, Anda memerlukan:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

RegistrationController

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
sumber
Info bagus, tapi memasukkan nama pengguna dan kata sandi ke url itu buruk. 1) tidak ada pelolosan yang dilakukan, jadi nama pengguna atau kata sandi dengan karakter khusus kemungkinan besar rusak, atau lebih buruk lagi, digunakan sebagai vektor eksploitasi keamanan. 2) kata sandi di url buruk karena url sering masuk ke disk, yang sangat buruk untuk keamanan - semua kata sandi Anda dalam teks biasa hanya ada di sana.
kambing
1

Saya mencoba untuk menguji aplikasi extjs dan setelah berhasil mengatur testingAuthenticationToken ini tiba-tiba berhenti bekerja tanpa sebab yang jelas.

Saya tidak bisa mendapatkan jawaban di atas untuk berfungsi jadi solusi saya adalah melewatkan sedikit pegas ini di lingkungan pengujian. Saya memperkenalkan jahitan di sekitar pegas seperti ini:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

Pengguna adalah tipe khusus di sini.

Saya kemudian membungkusnya di kelas yang hanya memiliki opsi untuk kode tes untuk beralih keluar.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

Versi uji terlihat seperti ini:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

Dalam kode panggilan saya masih menggunakan pengguna yang tepat yang dimuat dari database:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

Jelas ini tidak akan cocok jika Anda benar-benar perlu menggunakan keamanan tetapi saya menjalankan dengan pengaturan tanpa keamanan untuk penerapan pengujian. Saya pikir orang lain mungkin mengalami situasi yang sama. Ini adalah pola yang saya gunakan untuk mengejek dependensi statis sebelumnya. Alternatif lainnya adalah Anda dapat mempertahankan statisitas kelas pembungkus tetapi saya lebih suka yang satu ini karena dependensi kode lebih eksplisit karena Anda harus meneruskan CurrentUserAccessor ke dalam kelas yang memerlukannya.

JonnyRaa
sumber