RESTful Authentication przez Spring

262

Problem:
Mamy oparty na Spring MVC interfejs API RESTful, który zawiera poufne informacje. Interfejs API powinien być zabezpieczony, jednak wysyłanie poświadczeń użytkownika (kombinacja użytkownik / hasło) przy każdym żądaniu nie jest pożądane. Zgodnie z wytycznymi REST (i wewnętrznymi wymaganiami biznesowymi) serwer musi pozostać bezstanowy. Interfejs API zostanie wykorzystany przez inny serwer w podejściu typu mashup.

Wymagania:

  • Klient wysyła żądanie .../authenticate(niezabezpieczony adres URL) przy użyciu poświadczeń; serwer zwraca bezpieczny token, który zawiera wystarczającą ilość informacji, aby serwer mógł zweryfikować przyszłe żądania i pozostać bezstanowy. Prawdopodobnie składałoby się z tych samych informacji, co token Remember-Me firmy Spring Security .

  • Klient wysyła kolejne żądania do różnych (chronionych) adresów URL, dołączając wcześniej uzyskany token jako parametr zapytania (lub, co mniej pożądane, nagłówek żądania HTTP).

  • Od klienta nie można oczekiwać przechowywania plików cookie.

  • Ponieważ używamy już Springa, rozwiązanie powinno korzystać z Spring Security.

Waliliśmy głowami w ścianę, próbując sprawić, by to zadziałało, więc mam nadzieję, że ktoś już tam rozwiązał ten problem.

Biorąc pod uwagę powyższy scenariusz, w jaki sposób możesz rozwiązać tę konkretną potrzebę?

Chris Cashwell
źródło
49
Cześć Chris, nie jestem pewien, czy przekazanie tego tokena w parametrze zapytania jest najlepszym pomysłem. To pokaże się w logach, niezależnie od HTTPS lub HTTP. Nagłówki są prawdopodobnie bezpieczniejsze. Po prostu dla ciebie. Świetne pytanie. +1
jmort253
1
Jakie jest twoje zdanie na temat bezpaństwowców? Twój wymóg dotyczący tokena koliduje z moim rozumieniem bezpaństwowca. Odpowiedź uwierzytelnienia HTTP wydaje mi się jedyną bezstanową implementacją.
Markus Malkusch
9
@MarkusMalkusch bezpaństwowiec odnosi się do wiedzy serwera o wcześniejszej komunikacji z danym klientem. HTTP jest z definicji bezstanowy, a sesyjne pliki cookie sprawiają, że jest stanowy. Czas życia (i źródła) tokena są nieistotne; serwer dba tylko o to, czy jest poprawny i może być przywiązany do użytkownika (NIE sesji). Przekazywanie tokena identyfikującego nie zakłóca stanu.
Chris Cashwell
1
@ChrisCashwell W jaki sposób upewniasz się, że token nie jest sfałszowany / generowany przez klienta? Czy używasz klucza prywatnego po stronie serwera do szyfrowania tokena, przekazujesz go klientowi, a następnie używasz tego samego klucza do odszyfrowywania go podczas przyszłych żądań? Oczywiście Base64 lub inne zaciemnienie nie wystarczyłoby. Czy potrafisz opracować techniki „walidacji” tych tokenów?
Craig Otis
6
Chociaż jest to przestarzałe i nie dotknąłem ani nie zaktualizowałem kodu od ponad 2 lat, stworzyłem Gist, aby dalej rozwijać te koncepcje. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Odpowiedzi:

190

Udało nam się, aby działało dokładnie tak, jak opisano w PO, i mam nadzieję, że ktoś inny może skorzystać z rozwiązania. Oto co zrobiliśmy:

Skonfiguruj kontekst bezpieczeństwa w następujący sposób:

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

Jak widać, stworzyliśmy niestandardowy AuthenticationEntryPoint, który w zasadzie zwraca tylko 401 Unauthorizedjeśli żądanie nie zostało uwierzytelnione przez nasz łańcuch filtrów 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);
    }
}

Oczywiście TokenUtilszawiera poufny (i bardzo specyficzny dla danego przypadku) kod i nie można go łatwo udostępnić. Oto jego interfejs:

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

To powinno zapewnić ci dobry początek. Szczęśliwego kodowania. :)

Chris Cashwell
źródło
Czy konieczne jest uwierzytelnienie tokena, gdy token wysyła żądanie. Co powiesz na uzyskanie informacji o nazwie użytkownika bezpośrednio i ustawienie w bieżącym kontekście / żądaniu?
Fisher
1
@Spring Nigdzie ich nie przechowuję ... cała idea tokena polega na tym, że musi on być przekazywany przy każdym żądaniu i można go zdekonstruować (częściowo) w celu ustalenia jego ważności (stąd validate(...)metoda). Jest to ważne, ponieważ chcę, aby serwer pozostał bezstanowy. Wyobrażam sobie, że możesz zastosować to podejście bez potrzeby korzystania ze sprężyny.
Chris Cashwell
1
Jeśli klient jest przeglądarką, w jaki sposób można przechowywać token? czy musisz powtórzyć uwierzytelnianie dla każdego żądania?
beginner_
2
świetne wskazówki. @ChrisCashwell - nie mogę znaleźć części, w której weryfikujesz poświadczenia użytkownika i odsyłasz token? Domyślam się, że powinien to być gdzieś punkt końcowy / uwierzytelnienia punktu końcowego. czy mam rację ? Jeśli nie, jaki jest cel / uwierzytelnienia?
Yonatan Maman
3
co zawiera program AuthenticationManager?
MoienGK,
25

Możesz rozważyć uwierzytelnienie dostępu Digest . Zasadniczo protokół jest następujący:

  1. Żądanie jest wysyłane od klienta
  2. Serwer odpowiada unikalnym ciągiem nonce
  3. Klient podaje nazwę użytkownika i hasło (oraz niektóre inne wartości) md5 zaszyfrowane z nonce; ten skrót jest znany jako HA1
  4. Serwer jest wtedy w stanie zweryfikować tożsamość klienta i podać żądane materiały
  5. Komunikacja z nonce może być kontynuowana, dopóki serwer nie dostarczy nowej nonce (licznik służy do eliminacji ataków typu replay)

Cała ta komunikacja odbywa się za pomocą nagłówków, co, jak podkreśla jmort253, jest ogólnie bezpieczniejsze niż przekazywanie wrażliwych materiałów w parametrach adresu URL.

Uwierzytelnianie dostępu Digest jest obsługiwane przez Spring Security . Zauważ, że chociaż dokumenty mówią, że musisz mieć dostęp do hasła w postaci zwykłego tekstu, możesz pomyślnie uwierzytelnić się, jeśli masz skrót HA1 dla swojego klienta.

Tim Pote
źródło
1
Chociaż jest to możliwe podejście, kilka podróży w obie strony, które należy wykonać w celu odzyskania tokena, sprawia, że ​​jest to trochę niepożądane.
Chris Cashwell
Jeśli twój klient postępuje zgodnie ze specyfikacją uwierzytelniania HTTP, te podróże w obie strony mają miejsce tylko przy pierwszym wywołaniu i kiedy 5. nastąpi.
Markus Malkusch
5

Jeśli chodzi o tokeny zawierające informacje, tokeny internetowe JSON ( http://jwt.io ) to doskonała technologia. Główną koncepcją jest osadzenie elementów informacji (oświadczeń) w tokenie, a następnie podpisanie całego tokena, aby weryfikujący koniec mógł zweryfikować, czy oświadczenia są rzeczywiście godne zaufania.

Korzystam z tej implementacji Java: https://bitbucket.org/b_c/jose4j/wiki/Home

Istnieje również moduł Spring (spring-security-jwt), ale nie sprawdziłem, co obsługuje.

Leif John
źródło