RESTful Autentificare prin primăvară

Problem:
We have a Spring MVC-based RESTful API which contains sensitive information. The API should be secured, however sending the user's credentials (user/pass combo) with each request is not desirable. Per REST guidelines (and internal business requirements), the server must remain stateless. The API will be consumed by another server in a mashup-style approach.

Cerințe:

  • Clientul face o solicitare de .../authenticate (URL neprotejat) cu acreditări; serverul returnează un sigiliu securizat care conține suficiente informații pentru ca serverul să valideze cererile viitoare și să rămână apatrid. Acest lucru ar constitui, probabil, aceleași informații ca Amintește-ți-o pe Token .

  • Clientul face solicitări ulterioare către diferite adrese URL protejate, adăugând tokenul obținut anterior ca parametru de interogare (sau, mai puțin dorit, un antet de cerere HTTP).

  • Nu este de așteptat ca clientul să stocheze cookie-uri.

  • Din moment ce folosim deja Spring, soluția ar trebui să utilizeze securitatea de primăvară.

Ne-am lovit cu capul în fața peretelui, încercând să facem acest lucru, așa că sperăm că cineva acolo a rezolvat deja această problemă.

Având în vedere scenariul de mai sus, cum puteți rezolva această nevoie particulară?

229
@ChrisCashwell Cum vă asigurați că tokenul nu este falsificat/generat de client? Utilizați o cheie privată pe partea de server pentru a cripta tokenul, a le furniza clientului și apoi utilizați aceeași cheie pentru ao decripta în timpul cererilor viitoare? Evident, Base64 sau o altă obfuscare nu ar fi de ajuns. Puteți elabora tehnici de "validare" a acestor jetoane?
adăugat autor Craig Otis, sursa
Deși acest lucru este datat și nu am atins sau actualizat codul în mai mult de 2 ani, am creat un Gist pentru a extinde în continuare aceste concepte. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
adăugat autor Chris Cashwell, sursa
@CraigOtis o parte este plaintext, și unul este un hash md5 a acelor date și o cheie privată de pe server. ( email + delimiter + expiration_in_ms + delimiter + md5 de către utilizator, cu toate acestea tokenul ar fi nevalid deoarece hașcarea datelor modificate va avea ca rezultat o semnătură diferită față de a doua jumătate a jetonului. Trucul este să nu cripta jetonul; nu este nevoie să îl puteți inversa, trebuie doar să verificați integritatea. MD5 datele plaintext și comparați semnătura pentru egalitate.
adăugat autor Chris Cashwell, sursa
@MarkusMalkusch apatrid se referă la cunoștințele serverului privind comunicările anterioare cu un anumit client. HTTP este apatrid prin definiție, iar cookie-urile de sesiune o fac statală. Durata de viață (și sursa, de pildă) a simbolului este irelevantă; serverul are grijă de faptul că este valid și poate fi legat înapoi la un utilizator (NU este o sesiune). Prin urmare, trecerea unui jeton de identificare nu interferează cu starea de spirit.
adăugat autor Chris Cashwell, sursa
Bună Chris, nu sunt sigur că trecerea acestui simbol în parametrul de interogare este cea mai bună idee. Aceasta va apărea în jurnale, indiferent de HTTPS sau HTTP. Anteturile sunt probabil mai sigure. Doar FYI. Întrebare mare, totuși. +1
adăugat autor jmort253, sursa
Care este înțelegerea ta de apatrid? Cerința dvs. simbolică se ciocnește cu înțelegerea mea de apatrid. Răspunsul de autentificare Http mi se pare a fi singura implementare apatridă.
adăugat autor Markus Malkusch, sursa
@ChrisCashwell - M-ar ajuta cu adevărat dacă ați putea adăuga fișierele web.xml, root-context.xml și servlet-context.xml la gist prea.
adăugat autor kapad, sursa
@ChrisCashwell Am, de asemenea, același caz de utilizare ca al tău, am încercat să pună în aplicare pe springboot dar nu funcționează. Deci, dacă furnizați configurația bazei de pornire de primăvară, atunci ar fi foarte utilă.
adăugat autor Dhiren Hamal, sursa

4 răspunsuri

Am reușit să obținem acest lucru exact așa cum este descris în PO și sperăm că altcineva poate să folosească soluția. Iată ce am făcut:

Configurați contextul de securitate astfel:


    
    
    





    

După cum puteți vedea, am creat un AuthenticationEntryPoint personalizat, care în principiu returnează doar un cod 401 neautorizat dacă cererea nu a fost autentificată în lanțul de filtrare prin 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 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);
    }
}

Evident, TokenUtils conține unele closet (și foarte specifică fiecărui caz), cod și nu pot fi partajate cu ușurință. Iată interfața sa:

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

Asta ar trebui să te ducă la un început bun. Codificare fericită. :)

171
adăugat
@Chris Cashwell Bună Chris, atunci când creați un jeton în server unde le păstrați, într-o bază de date sau într-o fasole? de asemenea, eu folosesc Jee6, dar nu primăvara, pot folosi în continuare abordarea ta? tnx
adăugat autor Spring, sursa
Bună @ChrisCashwell .. Vă mulțumim pentru această soluție. Puteți indica alte soluții mai bune/mai inteligente pe care le-ați menționat într-unul din comentariile dvs.?
adăugat autor Anmol Gupta, sursa
Sentimentul meu este că este probabil cel mai bine să faceți autentificarea, deoarece vă permite să utilizați celelalte funcții de securitate de primăvară în restul aplicației dvs. atunci când faceți cereri REST. Adnotarea @Sercure este un exemplu care vine în minte.
adăugat autor threejeez, sursa
Corect, oricum, profitați de caracteristicile Spring Security și de aceea este benefic să folosiți această abordare. Am arătat asta ca răspuns la @Fisher.
adăugat autor threejeez, sursa
vă rugăm să trimiteți mai multe informații despre modul în care funcționează TokenUtils
adăugat autor zero01alpha, sursa
ce este în interiorul AuthenticationManager?
adăugat autor MoienGK, sursa
Bună, Chris, ți-am băgat de seamă, dar cred că e greșit (sper că am greșit). Deoarece parolele trebuie să fie stocate cel puțin în MD5 și când faceți noul UsernamePasswordAuthenticationToken (userDetails.getUsername (& zwnj;), userDetails.getPassword ()); Parola obținută este în MD5, astfel încât nu va trece niciodată filtrul de securitate. cum rezolvati asta ???
adăugat autor IturPablo, sursa
@ChrisCashwell - astfel încât parola nu poate fi recuperată din simbol. Asta a fost și intenția mea, mulțumesc pentru confirmare.
adăugat autor balas, sursa
Bună @ChrisCashwell, nu văd cum să implementez tokenUtils.getUserFromToken (...) așa cum mă aștept ca parola să fie spartă, astfel încât să nu poată fi recuperată?
adăugat autor balas, sursa
Bună @ChrisCashwell, este o ipoteză corectă că trebuie să implementăm un serviciu mapat la ceva de genul/autentifica, care primește doar numele de utilizator/permis combo și generează tokenul? Sau trebuie să fac manual ciclul de autentificare în acest moment? Nu este clar unde este executat ciclul de autentificare?
adăugat autor balas, sursa
Dumnezeu, @ChrisCashwell răspunsul dvs. ma ajutat atât de mult eu cant exprima asta .. Multumesc! O bere uriașă (sau sprite, dacă nu sunteți obișnuită să dringing) pentru dvs.: P
adăugat autor azalut, sursa
Vă rog să citiți întrebarea mea, este comună cu soluția dvs. stackoverflow.com/questions/18507816/& hellip;
adăugat autor Sergey, sursa
Dacă clientul este un browser, cum poate fi memorat tokenul? sau trebuie să refaceți autentificarea pentru fiecare solicitare?
adăugat autor beginner_, sursa
De asemenea, ceea ce am surprins @ChrisCashwell nu a menționat sau nu este necesitatea de a schimba punctele de intrare bazate pe cerere: org.springframework.security.web.authentication.DelegatingAu & zwnj; thenticationEntryPoi & zwnj; nt . Veți avea nevoie de acest lucru dacă aveți alte scheme complexe de autentificare precum OAuth.
adăugat autor Adam Gent, sursa
@ChrisCashwell - Caut sa implementez ceva foarte asemanator cu asta. Excelent gândire pusă în răspuns. Ai citit orice tutoriale sau docs pentru a obține aceste informații? Ce vrei sa spui prin "OP"? Căut în principal linkuri care să mă ajute să înțeleg pe deplin cum totul se blochează.
adăugat autor Thomas Buckley, sursa
după ce ați citit răspunsul de mai sus al @Chris Cashwell, verificați javacodegeeks.com/2014/10/…
adăugat autor oak, sursa
Aș dori, de asemenea, să subliniez că acest răspuns a fost postat acum trei ani și există, fără îndoială, modalități mai bune și/sau mai inteligente de a face acest lucru acum.
adăugat autor Chris Cashwell, sursa
@balas este corect, nu veți recupera nici o formă de parolă. Funcția getUserFromToken validează semnătura, apoi utilizează ID-ul de utilizator pentru a prelua utilizatorul. Din moment ce ID-ul se găsește în textul scris și este securizat de manipulare prin semnătura, acest lucru este tot ce ne trebuie. Știm că dacă executăm porțiunea de tip plaintext a tokenului furnizat de utilizator prin intermediul algoritmului de hashing al serverului și se compară cu tokenul complet ar trebui să obținem o potrivire exactă. Dacă da, am autentificat cu succes utilizatorul. Dacă nu, jetonul a
adăugat autor Chris Cashwell, sursa
@ Miles am folosit funcțiile de securitate de la Spring în întreaga aplicație, nu doar cu adnotarea @Secured . În schimb, am avut un context de securitate care a stabilit permisiunile pentru roluri.
adăugat autor Chris Cashwell, sursa
@IturPablo @mannuk - Nu sunt de acord ... Parolele noastre sunt, de asemenea, codate pe MD5, însă nu facem cu adevărat grijă despre parola lor atunci când facem autentificări bazate pe token. Pur și simplu creăm un obiect UserAuthentication și îl setăm pe SecurityContext , ocolind efectiv autentificarea prin parolă. Acesta este scopul în întregime de a avea un AuthenticationTokenProcessingFilter . TokenUtils este responsabil pentru validarea jetoanelor și determinarea codului Utilizator drept.
adăugat autor Chris Cashwell, sursa
@Spring nu le stocam nicăieri ... întreaga idee a simbolului este că trebuie să fie transmisă cu fiecare cerere și poate fi deconstruită (parțial) pentru a determina validitatea ei (de aceea validarea ). ..) ). Acest lucru este important pentru că vreau ca serverul să rămână apatrid. Mi-aș imagina că ați putea folosi această abordare fără a fi nevoie să folosiți primăvara.
adăugat autor Chris Cashwell, sursa
@ThomasBuckley - "OP" înseamnă "Post original", așa că mă refeream la întrebare. Nu pot spune exact ceea ce am citit, doar că am făcut o mulțime de lucruri în căutarea lanțului pentru metode de autentificare personalizate. Sper că vă ajută într-un fel sau altul.
adăugat autor Chris Cashwell, sursa
Încerc această soluție lângă securitatea normală bazată pe sesiuni pentru utilizatorii finali. Nu este definită nici o fasole unică de tip [org.springframework.security.web.context.SecurityContextRep & zwnj; ository]: asteptat un singur fasole de potrivire, dar a fost găsit 2: [org.springframework.security.web.context.NullSecurityContex & zwnj; tRepository # 0 , org.springframework.security.web.context.HttpSessionSecurity & zwnj; ContextRepository # 0]
adăugat autor Marc, sursa
Este necesar să autentificați tokenul atunci când jetonul trimite cu cererea. Ce zici de a obține informațiile despre numele de utilizator în mod direct și setați în contextul/cererea curentă?
adăugat autor Fisher, sursa
NM a dat seama: Puteți folosi @Secured cererea ulterioară va fi Authenticated în Spring Security - Tocmai am făcut UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken (userDetails.getUsername & zwnj;), userDetails.getPassword (), * userDetails.getAuthorities() *); și atribuirea rolului de autoritate utilizatorilor mei. Noroc Apreciez soluția!
adăugat autor jordan.baucke, sursa
@ChrisCashwell SecurityContextHolder.getContext() setAuthentication (authMan & zwnj; ager.authenticate (au & zwnj; thentication)); cererea ulterioară va fi Authenticated Adăugăm GrantedAuthority authority = new SimpleGrantedAuthority (user.getRole ()); și nu folosiți adnotarea @Secured ("ROLE_USER") securitatea detectează autorizarea curentă și o aplică și aplică regula @Secured ? Sau este contextul eliminat după?
adăugat autor jordan.baucke, sursa
@IturPablo Sunt cu adevărat de acord cu tine. Abordarea este în regulă, dar nu funcționează cu parolele codate cu md5. Dacă aveți parolele stocate în MD5 (în mod obișnuit) și în appContext-security.xml aveți furnizorul dvs. de autentificare cu parolă md5 nu este posibil să autentificați utilizatorii. Singurul lucru pe care este posibil să-l faceți este implementarea propriului manager de auth, deoarece în xml nu este posibil să definiți două (una md5 și una nu).
adăugat autor mannuk, sursa
sfaturi utile. @ChrisCashwell - partea pe care nu o pot găsi este locul unde validați acreditările utilizatorului și trimiteți un jeton înapoi? Mi-ar ghici că ar trebui să fie undeva în impl de/autentifica punct final. am dreptate ? Dacă nu, care este scopul/autentificarea?
adăugat autor Yonatan Maman, sursa
Max ChrisCashwell și @YonatanMaman, vă rog, ajutați-mi să oferiți partea în care validați acreditările utilizatorilor și trimiteți jetonul înapoi. ce este intrarea în fișierul secutiry.xml și în codul java pentru asta?
adăugat autor sunny_dev, sursa
@ChrisCashwell, esti AWESOME. Am căutat peste tot pe internet să-mi dau seama cum să mă descurc și să nu am noroc. Datorită soluției dvs., am reușit în sfârșit să lucrez.
adăugat autor Bashir, sursa
@ChrisCashwell - Vă rog să detaliați cum este trimis jetonul înapoi la utilizator ca răspuns la autentificarea apelului API? După cum am înțeles implementăm serviciul CustomAuthenticationManager și Serviciul de Detalii utilizator pentru a vă asigura că acreditările utilizatorului sunt corecte inițial, dar în ce componentă ar trebui să generăm răspunsul Json care conține token pentru utilizator
adăugat autor Shailesh Vaishampayan, sursa
@ChrisCashwell Dacă nu stocați jetoane pentru fiecare utilizator, cum identificați un utilizator care a ieșit înainte să expire tokenul său?
adăugat autor Shailesh Vaishampayan, sursa

S-ar putea să vă gândiți la Autentificarea accesului digest . În esență, protocolul este următorul:

  1. Solicitarea se face din client
  2. Serverul răspunde cu un șir unic nonce
  3. Clientul furnizează un nume de utilizator și o parolă (și alte valori) md5 hashed cu nonce; acest hash este cunoscut ca HA1
  4. Serverul poate verifica identitatea clientului și poate servi materialele solicitate
  5. Comunicarea cu nonce poate continua până când serverul furnizează un nou nonce (un numărător este folosit pentru a elimina atacurile de replay)

Toate aceste comunicări se fac prin anteturi, care, după cum subliniază jmort253, sunt în general mai sigure decât comunicarea materialelor sensibile în parametrii url.

Autentificarea accesului prin Digest este susținută de Security de primăvară . Observați că, deși documentele spun că trebuie să aveți acces la parola de text simplu a clientului dvs., puteți autentifica cu succes dacă aveți HA1 hash pentru clientul tău.

18
adăugat
Deși aceasta este o posibilă abordare, mai multe călătorii rotunde care trebuie făcute pentru a obține un token o face puțin cam nedorită.
adăugat autor Chris Cashwell, sursa
Dacă clientul dvs. respectă specificația HTTP Authentication, aceste călătorii rotunde se întâmplă numai la primul apel și când se întâmplă 5.
adăugat autor Markus Malkusch, sursa

În ceea ce privește token-urile care transportau informații, JSON Web Tokens ( http://jwt.io ) este o tehnologie genială. Conceptul principal este de a încorpora elementele de informație (revendicări) în token și apoi de a semna întreaga token, astfel încât sfârșitul validării să poată verifica dacă revendicările sunt într-adevăr demne de încredere.

Folosesc această implementare Java: https://bitbucket.org/b_c/jose4j/wiki/Home

Există, de asemenea, un modul de primăvară (spring-security-jwt), dar nu am analizat ceea ce susține.

4
adăugat

De ce nu începeți să utilizați OAuth cu JSON WebTokens

http://projects.spring.io/spring-security-oauth/

OAuth2 este un protocol/cadru standardizat de autorizare. În conformitate cu versiunea oficială OAuth2 Specificație :

Puteți găsi mai multe informații aici

1
adăugat