Otentikasi tenang melalui musim semi

262

Masalah:
Kami memiliki RESTful API Spring MVC berbasis yang berisi informasi sensitif. API harus diamankan, namun mengirimkan kredensial pengguna (kombo / pengguna) dengan setiap permintaan tidak diinginkan. Per pedoman REST (dan persyaratan bisnis internal), server harus tetap tanpa kewarganegaraan. API akan dikonsumsi oleh server lain dengan pendekatan gaya mashup.

Persyaratan:

  • Klien membuat permintaan ke .../authenticate(URL yang tidak dilindungi) dengan kredensial; server mengembalikan token aman yang berisi informasi yang cukup bagi server untuk memvalidasi permintaan di masa mendatang dan tetap tanpa kewarganegaraan. Ini kemungkinan terdiri dari informasi yang sama dengan Remember-Me Token Spring Security .

  • Klien membuat permintaan berikutnya ke berbagai URL (terlindungi), menambahkan token yang diperoleh sebelumnya sebagai parameter kueri (atau, yang kurang diinginkan, header permintaan HTTP).

  • Klien tidak dapat diharapkan untuk menyimpan cookie.

  • Karena kita sudah menggunakan Spring, solusinya harus menggunakan Spring Security.

Kami telah membenturkan kepala kami ke dinding mencoba membuat ini bekerja, jadi semoga seseorang di luar sana telah menyelesaikan masalah ini.

Dengan skenario di atas, bagaimana Anda bisa menyelesaikan kebutuhan khusus ini?

Chris Cashwell
sumber
49
Halo Chris, saya tidak yakin memberikan token pada parameter kueri adalah ide terbaik. Itu akan muncul di log, terlepas dari HTTPS atau HTTP. Header mungkin lebih aman. Hanya FYI. Pertanyaan yang bagus. +1
jmort253
1
Apa pemahaman Anda tentang kewarganegaraan? Persyaratan token Anda bertabrakan dengan pemahaman saya tentang kewarganegaraan. Jawaban otentikasi Http menurut saya satu-satunya implementasi tanpa kewarganegaraan.
Markus Malkusch
9
@MarkusMalkusch stateless mengacu pada pengetahuan server tentang komunikasi sebelumnya dengan klien yang diberikan. HTTP adalah stateless menurut definisi, dan cookie sesi membuatnya stateful. Masa pakai (dan sumber, dalam hal ini) dari token tidak relevan; server hanya peduli bahwa itu valid dan dapat diikat kembali ke pengguna (BUKAN sesi). Melewati token pengidentifikasi, oleh karena itu, tidak mengganggu keadaan.
Chris Cashwell
1
@ChrisCashwell Bagaimana Anda memastikan bahwa token tidak dipalsukan / dibuat oleh klien? Apakah Anda menggunakan kunci pribadi di sisi server untuk mengenkripsi token, memberikannya kepada klien, dan kemudian menggunakan kunci yang sama untuk mendekripsi itu selama permintaan di masa depan? Jelas Base64 atau kebingungan lainnya tidak akan cukup. Bisakah Anda menguraikan teknik untuk "validasi" token ini?
Craig Otis
6
Meskipun ini tanggal dan saya belum menyentuh atau memperbarui kode dalam lebih dari 2 tahun, saya telah membuat Intisari untuk lebih memperluas konsep-konsep ini. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Jawaban:

190

Kami berhasil agar ini berfungsi persis seperti yang dijelaskan dalam OP, dan mudah-mudahan orang lain dapat memanfaatkan solusinya. Inilah yang kami lakukan:

Atur konteks keamanan seperti ini:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

Seperti yang Anda lihat, kami telah membuat sebuah kebiasaan AuthenticationEntryPoint, yang pada dasarnya hanya mengembalikan a 401 Unauthorizedjika permintaan tidak diautentikasi dalam rantai filter oleh kami AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

Jelas, TokenUtilsberisi beberapa kode rahasia (dan sangat khusus kasus) dan tidak dapat dibagikan dengan mudah. Inilah antarmuka-nya:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Itu seharusnya membuat Anda memulai dengan baik. Selamat coding. :)

Chris Cashwell
sumber
Apakah perlu untuk mengotentikasi token ketika token mengirim dengan permintaan. Bagaimana dengan mendapatkan info nama pengguna secara langsung dan diatur dalam konteks / permintaan saat ini?
Fisher
1
@Spring Saya tidak menyimpannya di mana saja ... ide token keseluruhannya adalah bahwa token itu harus diteruskan dengan setiap permintaan, dan dapat didekonstruksi (sebagian) untuk menentukan validitasnya (karenanya validate(...)metodenya). Ini penting karena saya ingin server tetap stateless. Saya akan membayangkan Anda bisa menggunakan pendekatan ini tanpa harus menggunakan Spring.
Chris Cashwell
1
Jika klien adalah browser, bagaimana token dapat disimpan? atau apakah Anda harus mengulang otentikasi untuk setiap permintaan?
beginner_
2
tips hebat @ChrisCashwell - bagian yang tidak dapat saya temukan adalah di mana Anda memvalidasi kredensial pengguna dan mengirim kembali token? Saya kira itu harus berada di suatu tempat di int / otentikasi titik akhir. Apakah saya benar ? Jika tidak, apa tujuan / otentikasi?
Yonatan Maman
3
apa yang ada di dalam AuthenticationManager?
MoienGK
25

Anda mungkin mempertimbangkan Otentikasi Akses Intisari . Intinya protokol adalah sebagai berikut:

  1. Permintaan dibuat dari klien
  2. Server merespons dengan string nonce unik
  3. Klien menyediakan nama pengguna dan kata sandi (dan beberapa nilai lainnya) md5 hash dengan thecece; hash ini dikenal sebagai HA1
  4. Server kemudian dapat memverifikasi identitas klien dan menyajikan materi yang diminta
  5. Komunikasi dengan nonce dapat berlanjut sampai server memasok nonce baru (penghitung digunakan untuk menghilangkan serangan replay)

Semua komunikasi ini dilakukan melalui tajuk, yang, seperti ditunjukkan jmort253, umumnya lebih aman daripada mengomunikasikan materi sensitif dalam parameter url.

Otentikasi Akses Digest didukung oleh Spring Security . Perhatikan bahwa, meskipun dokumen mengatakan bahwa Anda harus memiliki akses ke kata sandi teks biasa klien Anda, Anda dapat berhasil mengotentikasi jika Anda memiliki hash HA1 untuk klien Anda.

Tim Pote
sumber
1
Meskipun ini merupakan pendekatan yang memungkinkan, beberapa perjalanan bolak-balik yang harus dilakukan untuk mengambil token membuatnya sedikit tidak diinginkan.
Chris Cashwell
Jika klien Anda mengikuti spesifikasi Autentikasi HTTP, round trip tersebut hanya terjadi setelah panggilan pertama dan ketika 5. terjadi.
Markus Malkusch
5

Mengenai token yang membawa informasi, JSON Web Tokens ( http://jwt.io ) adalah teknologi yang brilian. Konsep utamanya adalah memasukkan elemen informasi (klaim) ke dalam token, dan kemudian menandatangani seluruh token sehingga akhir yang valid dapat memverifikasi bahwa klaim tersebut memang dapat dipercaya.

Saya menggunakan implementasi Java ini: https://bitbucket.org/b_c/jose4j/wiki/Home

Ada juga modul Spring (spring-security-jwt), tapi saya belum melihat apa yang didukungnya.

Leif John
sumber