使用RestTemplate进行Spring安全身份validation

我有2个春季网络应用程序,提供2组独立的服务。 Web App 1使用基于用户的身份validation实现Spring Security。

现在,Web App 2需要访问Web App 1的服务。通常,我们将使用RestTemplate类向其他Web服务发出请求。

我们如何将Web App 2请求中的身份validation凭据传递给Web App 1

我处于同样的境地。 这是我的解决方案。

服务器 – 弹簧安全配置

            

客户端RestTemplate配置

                  

自定义HttpState实现

 /** * 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); } } 

Maven依赖

  commons-httpclient commons-httpclient 3.1  

这是一个适用于Spring 3.1和Apache HttpComponents 4.1的解决方案。我在此站点上创建了基于各种答案并阅读Spring RestTempalte源代码。 我希望能够节省他人的时间,我认为Spring应该只有一些内置的代码,但事实并非如此。

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

下面是Factory类,它使用RestTemplate在每个请求上设置HttpComponents上下文。

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

下面是Statefull Rest模板,您可以使用它来记住cookie,一旦您使用它登录,它将记住JSESSIONID并在后续请求中发送它。

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

这是一个代表rest客户端的类,以便您可以调用使用spring security保护的应用程序。

 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 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 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(); } } 

RestTemplate非常基础且有限; 似乎没有一种简单的方法可以做到这一点。 最好的方法可能是在Web App 1中实现基本身份validation的摘要。然后直接使用Apache HttpClient从Web App 2访问其余服务。

话虽这么说,为了测试我能够通过一个大的黑客来解决这个问题。 基本上,您使用RestTemplate提交登录(j_spring_security_check),从请求标头中解析出jsessionid,然后提交其余请求。 这是代码,但我怀疑它是生产就绪代码的最佳解决方案。

 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() { @Override public String extractData(ClientHttpResponse response) throws IOException { List 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()); } 

}

请注意,要使用此function,您需要在JCA中创建信任存储,以便实际建立SSL连接。 我假设您不希望Spring Security的登录在生产站点的普通HTTP上,因为这将是一个巨大的安全漏洞。

如果您正在寻找简单的呼叫而不是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); 

以下将validation并返回会话cookie:

 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); }); 

当前经过身份validation的用户凭据应该在Authentication对象上的Web App 1中可用,可以通过SecurityContext访问(例如,您可以通过调用SecurityContextHolder.getContext().getAuthentication()来检索它。

检索凭据后,您可以使用它们来访问Web App 2。

您可以通过使用装饰器(如此处所述)或使用RestTemplate.exchange()方法扩展RestTemplate来传递“Authentiation”标头,如本论坛post中所述 。

这与ams的方法非常相似,除了我完全封装了在StatefulClientHttpRequestFactory中维护会话cookie的问题。 此外,通过使用此行为装饰现有ClientHttpRequestFactory,它可以与任何底层ClientHttpRequestFactory一起使用,并且不绑定到特定实现。

 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 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 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 filter; public CallbackClientHttpRequest(ClientHttpRequest request, Function 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(); } } }