2011-01-06 18 views
10

Mam 2 aplikacje internetowe wiosenne, które zapewniają 2 oddzielne zestawy usług. Aplikacja internetowa 1 ma Spring Security zaimplementowane przy użyciu uwierzytelniania użytkownika.Spring Security Authentication przy użyciu RestTemplate

Teraz aplikacja internetowa 2 musi uzyskać dostęp do usługi aplikacji internetowej 1. Zwykle używamy klasy RestTemplate do wysyłania żądań do innych usług internetowych.

Jak mijamy poświadczenia uwierzytelniania w żądaniu Web App 2 do Web App 1

Odpowiedz

9

Byłem w tej samej sytuacji. Oto moje rozwiązanie.

Server - wiosna config bezpieczeństwo

<sec:http> 
    <sec:intercept-url pattern="/**" access="ROLE_USER" method="POST"/> 
    <sec:intercept-url pattern="/**" filters="none" method="GET"/> 
    <sec:http-basic /> 
</sec:http> 

<sec:authentication-manager alias="authenticationManager"> 
    <sec:authentication-provider> 
     <sec:user-service> 
      <sec:user name="${rest.username}" password="${rest.password}" authorities="ROLE_USER"/> 
     </sec:user-service> 
    </sec:authentication-provider> 
</sec:authentication-manager> 

po stronie klienta RestTemplate config

<bean id="httpClient" class="org.apache.commons.httpclient.HttpClient"> 
    <constructor-arg ref="httpClientParams"/> 
    <property name="state" ref="httpState"/> 
</bean> 

<bean id="httpState" class="CustomHttpState"> 
    <property name="credentials" ref="credentials"/> 
</bean> 

<bean id="credentials" class="org.apache.commons.httpclient.UsernamePasswordCredentials"> 
    <constructor-arg value="${rest.username}"/> 
    <constructor-arg value="${rest.password}"/> 
</bean> 

<bean id="httpClientFactory" class="org.springframework.http.client.CommonsClientHttpRequestFactory"> 
    <constructor-arg ref="httpClient"/> 
</bean> 


<bean class="org.springframework.web.client.RestTemplate"> 
    <constructor-arg ref="httpClientFactory"/>     
</bean> 

klienta HttpState realizacja

/** 
* Custom implementation of {@link HttpState} with credentials property. 
* 
* @author banterCZ 
*/ 
public class CustomHttpState extends HttpState { 

    /** 
    * Set credentials property. 
    * 
    * @param credentials 
    * @see #setCredentials(org.apache.commons.httpclient.auth.AuthScope, org.apache.commons.httpclient.Credentials) 
    */ 
    public void setCredentials(final Credentials credentials) { 
     super.setCredentials(AuthScope.ANY, credentials); 
    } 

} 

zależność Maven

<dependency> 
    <groupId>commons-httpclient</groupId> 
    <artifactId>commons-httpclient</artifactId> 
    <version>3.1</version> 
</dependency> 
+0

Czy możesz wyjaśnić, dlaczego potrzebna jest niestandardowa klasa stanu http? – kamaci

+0

@kamaci HttpState # setCredentials nie jest ustawiaczem (znany również jako accessor), ponieważ wymaga dwóch parametrów. Tak więc poświadczenia nie są polem POJO i nie można uzyskać do nich dostępu w konfiguracji Spring xml. – banterCZ

+0

Po uruchomieniu aplikacji dziennik rejestruje, że: [org.springframework.beans.GenericTypeAwarePropertyDescriptor] - [Nieprawidłowe poświadczenia właściwości JavaBean są dostępne! Niejednoznaczne metody zapisu znaleziono obok faktycznie używanego [public void a.b.c.dustomHttpState.setCredentials (org.apache.commons.httpclient.Credentials)]:: ... (błąd trwa) '. Czy to zwykle? – kamaci

1

aktualnie uwierzytelnionych poświadczeń użytkownika powinny być dostępne w sieci Web App 1 na Authentication obiektu, który jest dostępny za pośrednictwem SecurityContext (dla na przykład można go pobrać, dzwoniąc pod numer SecurityContextHolder.getContext().getAuthentication()).

Po pobierania poświadczeń, można z nich korzystać w celu uzyskania dostępu Web App 2.

Można przekazać „Authentiation” nagłówek z RestTemplate albo poprzez rozszerzenie go o dekoratora (jak opisano here) lub przy użyciu metody RestTemplate.exchange() , jak opisano in this forum post.

+0

Zakładam, że aplikacja internetowa 2 nie może zobaczyć sesji HTTP dla aplikacji internetowej 1, więc prawdopodobnie to nie zadziała. – AngerClown

+0

Niestety, źle zrozumiałem pożądany kierunek: * od * aplikacji sieci Web2 * do * aplikacji internetowej 1. Zmiana mojej odpowiedzi. –

3

The RestTemplate jest bardzo prosty i ograniczony; wydaje się, że nie ma na to łatwego sposobu. Najlepszym sposobem jest prawdopodobnie zaimplementowanie skrótu podstawowego uwierzytelnienia w aplikacji internetowej 1. Następnie użyj Apache HttpClient bezpośrednio, aby uzyskać dostęp do pozostałych usług z aplikacji sieci Web 2.

Powiedziawszy, że do testów udało mi się obejść ten problem za pomocą wielki hack. Zasadniczo używasz RestTemplate, aby przesłać login (j_spring_security_check), przetworzyć jsessionid z nagłówków żądań, a następnie przesłać żądanie odpoczynku. Oto kod, ale wątpię, że jest to najlepsze rozwiązanie dla kodu produkcyjnego.

public final class RESTTest { 
    public static void main(String[] args) { 
    RestTemplate rest = new RestTemplate(); 

    HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 
     @Override 
     public boolean verify(String s, SSLSession sslsession) { 
      return true; 
     } 
    }); 

    // setting up a trust store with JCA is a whole other issue 
    // this assumes you can only log in via SSL 
    // you could turn that off, but not on a production site! 
    System.setProperty("javax.net.ssl.trustStore", "/path/to/cacerts"); 
    System.setProperty("javax.net.ssl.trustStorePassword", "somepassword"); 

    String jsessionid = rest.execute("https://localhost:8443/j_spring_security_check", HttpMethod.POST, 
      new RequestCallback() { 
       @Override 
       public void doWithRequest(ClientHttpRequest request) throws IOException { 
       request.getBody().write("j_username=user&j_password=user".getBytes()); 
       } 
      }, new ResponseExtractor<String>() { 
       @Override 
       public String extractData(ClientHttpResponse response) throws IOException { 
        List<String> cookies = response.getHeaders().get("Cookie"); 

        // assuming only one cookie with jsessionid as the only value 
        if (cookies == null) { 
         cookies = response.getHeaders().get("Set-Cookie"); 
        } 

        String cookie = cookies.get(cookies.size() - 1); 

        int start = cookie.indexOf('='); 
        int end = cookie.indexOf(';'); 

        return cookie.substring(start + 1, end); 
       } 
      }); 

    rest.put("http://localhost:8080/rest/program.json;jsessionid=" + jsessionid, new DAO("REST Test").asJSON()); 
} 

}

Uwaga dla tej pracy, trzeba stworzyć sklep Zaufaj JCA więc połączenie SSL może być faktycznie wykonane. Zakładam, że nie chcesz, aby login Spring Security był nad zwykłym HTTP dla strony produkcyjnej, ponieważ byłaby to ogromna luka w zabezpieczeniach.

+0

Zdaję sobie sprawę, że odpowiedziałeś na to ponad rok temu, ale chciałbym Cię poprosić o "opublikowanie kodu", jeśli nadal jest dostępny. :) thanks – Justin

+0

Kod opublikowany, ale nie próbowałem go ponownie z najnowszą wersją Spring. Czy RestTemplate nie został zaktualizowany? – AngerClown

12

Oto rozwiązanie, które działa bardzo dobrze z wiosny 3.1 i Apache HttpComponents 4,1 ja stworzony w oparciu różne odpowiedzi na temat tej strony i odczytanie kodu źródłowego wiosna RestTempalte. Dzielę się w nadziei na ocalenie czasu innych osób, myślę, że wiosna powinna mieć wbudowany jakiś kod, ale tak nie jest.

RestClient client = new RestClient(); 
client.setApplicationPath("someApp"); 
String url = client.login("theuser", "123456"); 
UserPortfolio portfolio = client.template().getForObject(client.apiUrl("portfolio"), 
         UserPortfolio.class); 

Poniżej jest klasa fabryczne, który ustawia się kontekst HttpComponents być taka sama na każde żądanie z RestTemplate.

public class StatefullHttpComponentsClientHttpRequestFactory extends 
        HttpComponentsClientHttpRequestFactory 
{ 
    private final HttpContext httpContext; 

    public StatefullHttpComponentsClientHttpRequestFactory(HttpClient httpClient, HttpContext httpContext) 
    { 
     super(httpClient); 
     this.httpContext = httpContext; 
    } 

    @Override 
    protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) 
    { 
     return this.httpContext; 
    } 
} 

Poniżej statefull Reszta szablon, który można użyć, aby pamiętać, ciasteczka, po zalogowaniu się z nim pamięta JSESSIONID i wysłał go na kolejne żądania.

public class StatefullRestTemplate extends RestTemplate 
{ 
    private final HttpClient httpClient; 
    private final CookieStore cookieStore; 
    private final HttpContext httpContext; 
    private final StatefullHttpComponentsClientHttpRequestFactory statefullHttpComponentsClientHttpRequestFactory; 

    public StatefullRestTemplate() 
    { 
     super(); 
     httpClient = new DefaultHttpClient(); 
     cookieStore = new BasicCookieStore(); 
     httpContext = new BasicHttpContext(); 
     httpContext.setAttribute(ClientContext.COOKIE_STORE, getCookieStore()); 
     statefullHttpComponentsClientHttpRequestFactory = new StatefullHttpComponentsClientHttpRequestFactory(httpClient, httpContext); 
     super.setRequestFactory(statefullHttpComponentsClientHttpRequestFactory); 
    } 

    public HttpClient getHttpClient() 
    { 
     return httpClient; 
    } 

    public CookieStore getCookieStore() 
    { 
     return cookieStore; 
    } 

    public HttpContext getHttpContext() 
    { 
     return httpContext; 
    } 

    public StatefullHttpComponentsClientHttpRequestFactory getStatefulHttpClientRequestFactory() 
    { 
     return statefullHttpComponentsClientHttpRequestFactory; 
    } 
} 

Oto klasy do reprezentowania klienta odpoczynku tak, że można zadzwonić do aplikacji zabezpieczonej wiosna bezpieczeństwa.

public class RestClient 
{ 
    private String host = "localhost"; 
    private String port = "8080"; 
    private String applicationPath; 
    private String apiPath = "api"; 
    private String loginPath = "j_spring_security_check"; 
    private String logoutPath = "logout"; 
    private final String usernameInputFieldName = "j_username"; 
    private final String passwordInputFieldName = "j_password"; 
    private final StatefullRestTemplate template = new StatefullRestTemplate(); 

    /** 
    * This method logs into a service by doing an standard http using the configuration in this class. 
    * 
    * @param username 
    *   the username to log into the application with 
    * @param password 
    *   the password to log into the application with 
    * 
    * @return the url that the login redirects to 
    */ 
    public String login(String username, String password) 
    { 
     MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); 
     form.add(usernameInputFieldName, username); 
     form.add(passwordInputFieldName, password); 
     URI location = this.template.postForLocation(loginUrl(), form); 
     return location.toString(); 
    } 

    /** 
    * Logout by doing an http get on the logout url 
    * 
    * @return result of the get as ResponseEntity 
    */ 
    public ResponseEntity<String> logout() 
    { 
     return this.template.getForEntity(logoutUrl(), String.class); 
    } 

    public String applicationUrl(String relativePath) 
    { 
     return applicationUrl() + "/" + checkNotNull(relativePath); 
    } 

    public String apiUrl(String relativePath) 
    { 
     return applicationUrl(apiPath + "/" + checkNotNull(relativePath)); 
    } 

    public StatefullRestTemplate template() 
    { 
     return template; 
    } 

    public String serverUrl() 
    { 
     return "http://" + host + ":" + port; 
    } 

    public String applicationUrl() 
    { 
     return serverUrl() + "/" + nullToEmpty(applicationPath); 
    } 

    public String loginUrl() 
    { 
     return applicationUrl(loginPath); 
    } 

    public String logoutUrl() 
    { 
     return applicationUrl(logoutPath); 
    } 

    public String apiUrl() 
    { 
     return applicationUrl(apiPath); 
    } 

    public void setLogoutPath(String logoutPath) 
    { 
     this.logoutPath = logoutPath; 
    } 

    public String getHost() 
    { 
     return host; 
    } 

    public void setHost(String host) 
    { 
     this.host = host; 
    } 

    public String getPort() 
    { 
     return port; 
    } 

    public void setPort(String port) 
    { 
     this.port = port; 
    } 

    public String getApplicationPath() 
    { 
     return applicationPath; 
    } 

    public void setApplicationPath(String contextPath) 
    { 
     this.applicationPath = contextPath; 
    } 

    public String getApiPath() 
    { 
     return apiPath; 
    } 

    public void setApiPath(String apiPath) 
    { 
     this.apiPath = apiPath; 
    } 

    public String getLoginPath() 
    { 
     return loginPath; 
    } 

    public void setLoginPath(String loginPath) 
    { 
     this.loginPath = loginPath; 
    } 

    public String getLogoutPath() 
    { 
     return logoutPath; 
    } 

    @Override 
    public String toString() 
    { 
     StringBuilder builder = new StringBuilder(); 
     builder.append("RestClient [\n serverUrl()="); 
     builder.append(serverUrl()); 
     builder.append(", \n applicationUrl()="); 
     builder.append(applicationUrl()); 
     builder.append(", \n loginUrl()="); 
     builder.append(loginUrl()); 
     builder.append(", \n logoutUrl()="); 
     builder.append(logoutUrl()); 
     builder.append(", \n apiUrl()="); 
     builder.append(apiUrl()); 
     builder.append("\n]"); 
     return builder.toString(); 
    } 
} 
2

Jest prosty sposób na zrobienie tego w przypadku, gdy szukasz kogoś, kto szuka prostego połączenia, a nie konsumenta API.

HttpClient client = new HttpClient(); 
    client.getParams().setAuthenticationPreemptive(true); 
    Credentials defaultcreds = new UsernamePasswordCredentials("username", "password"); 
    RestTemplate restTemplate = new RestTemplate(); 
    restTemplate.setRequestFactory(new CommonsClientHttpRequestFactory(client)); 
    client.getState().setCredentials(AuthScope.ANY, defaultcreds); 
+0

To wydaje się być konfigurowanie RestTemplate RequestFactory, co się dzieje, gdy chcesz uzyskać dostęp do wielu adresów URL, gdzie wszystkie z nich wymagają różnych uwierzytelniania digest. Możesz dodać na hosta auth/lub musisz użyć nowego restTemplate? Czy mógłbyś podać przykład: –

+0

Myślę, że musiałbyś utworzyć szablon restTemplate dla każdego typu streszczenia, lub mógłbyś rozszerzyć 'CommonsClientHttpRequestFactory' iw zależności od żądania określić, który klient użyje – Panthro

+0

dziękuję, ten kod pomógł mi rozwiązać mój problem –

2

Następujące będzie uwierzytelniać i powrót cookie sesji:

String sessionCookie= restTemplate.execute(uri, HttpMethod.POST, request -> { 
     request.getBody().write(("j_username=USER_NAME&j_password=PASSWORD").getBytes()); 
    }, response -> { 
     AbstractClientHttpResponse r = (AbstractClientHttpResponse) response; 
     HttpHeaders headers = r.getHeaders(); 
     return headers.get("Set-Cookie").get(0); 
    }); 
0

Jest to bardzo podobne do podejścia AMS, z wyjątkiem mam całkowicie obudowane obawy utrzymania cookie sesji w StatefulClientHttpRequestFactory. Również poprzez dekorowanie istniejącego ClientHttpRequestFactory tym zachowaniem, można go użyć z dowolnym klientem ClientHttpRequestFactory i nie jest związany z konkretną implementacją.

import org.apache.commons.logging.Log; 
import org.apache.commons.logging.LogFactory; 
import org.springframework.http.HttpHeaders; 
import org.springframework.http.HttpMethod; 
import org.springframework.http.client.ClientHttpRequest; 
import org.springframework.http.client.ClientHttpRequestFactory; 
import org.springframework.http.client.ClientHttpResponse; 

import java.io.IOException; 
import java.io.OutputStream; 
import java.net.URI; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 
import java.util.function.Function; 
import java.util.stream.Collectors; 

import static java.lang.String.format; 

/** 
* Decorates a ClientHttpRequestFactory to maintain sessions (cookies) 
* to web servers. 
*/ 
public class StatefulClientHttpRequestFactory implements ClientHttpRequestFactory { 
    protected final Log logger = LogFactory.getLog(this.getClass()); 

    private final ClientHttpRequestFactory requestFactory; 
    private final Map<String, String> hostToCookie = new HashMap<>(); 

    public StatefulClientHttpRequestFactory(ClientHttpRequestFactory requestFactory){ 
     this.requestFactory = requestFactory; 
    } 

    @Override 
    public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { 

     ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); 

     final String host = request.getURI().getHost(); 
     String cookie = getCookie(host); 
     if(cookie != null){ 
      logger.debug(format("Setting request Cookie header to [%s]", cookie)); 
      request.getHeaders().set("Cookie", cookie); 
     } 

     //decorate the request with a callback to process 'Set-Cookie' when executed 
     return new CallbackClientHttpRequest(request, response -> { 
      List<String> responseCookie = response.getHeaders().get("Set-Cookie"); 
      if(responseCookie != null){ 
       setCookie(host, responseCookie.stream().collect(Collectors.joining("; "))); 
      } 
      return response; 
     }); 
    } 

    private synchronized String getCookie(String host){ 
     String cookie = hostToCookie.get(host); 
     return cookie; 
    } 

    private synchronized void setCookie(String host, String cookie){ 
     hostToCookie.put(host, cookie); 
    } 

    private static class CallbackClientHttpRequest implements ClientHttpRequest{ 

     private final ClientHttpRequest request; 
     private final Function<ClientHttpResponse, ClientHttpResponse> filter; 

     public CallbackClientHttpRequest(ClientHttpRequest request, Function<ClientHttpResponse, ClientHttpResponse> filter){ 
      this.request = request; 
      this.filter = filter; 
     } 

     @Override 
     public ClientHttpResponse execute() throws IOException { 
      ClientHttpResponse response = request.execute(); 
      return filter.apply(response); 
     } 

     @Override 
     public OutputStream getBody() throws IOException { 
      return request.getBody(); 
     } 

     @Override 
     public HttpMethod getMethod() { 
      return request.getMethod(); 
     } 

     @Override 
     public URI getURI() { 
      return request.getURI(); 
     } 

     @Override 
     public HttpHeaders getHeaders() { 
      return request.getHeaders(); 
     } 
    } 
} 
Powiązane problemy