#java #spring #authentication #https #resttemplate
#java #spring #аутентификация #spring-безопасность
Вопрос:
У меня есть 2 веб-приложения Spring, которые предоставляют 2 отдельных набора сервисов. В веб-приложении 1 Spring Security реализована с использованием аутентификации на основе пользователя.
Теперь веб-приложению 2 необходимо получить доступ к службе веб-приложения 1. Обычно мы используем класс RestTemplate для выполнения запросов к другим веб-службам.
Как нам передать учетные данные для аутентификации в запросе веб-приложения 2 веб-приложению 1
Ответ №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);
Ниже приведен заводской класс, который настраивает контекст HttpComponents таким образом, чтобы он был одинаковым при каждом запросе с 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;
}
}
Ниже приведен шаблон 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
безопасность.
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();
}
}
Комментарии:
1. Для тех пользователей, которые хотят использовать этот код в Android SDK 23 , пожалуйста, добавьте зависимость gradle «org.apache.httpcomponents: httpcore:4.3.2» в файл вашего модуля gradle.
Ответ №2:
Я был в такой же ситуации. Вот мое решение.
Сервер — spring security config
<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>
Конфигурация RestTemplate на стороне клиента
<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>
Пользовательская реализация 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
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
Комментарии:
1. Можете ли вы объяснить, зачем нужен пользовательский класс состояния http?
2. @kamaci HttpState#setCredentials не является установщиком (также известным как средство доступа), поскольку требует двух параметров. Таким образом, учетные данные не являются полем POJO и к ним нельзя получить доступ в Spring xml config.
3. Когда я запускаю свое приложение, оно регистрирует это:
[org.springframework.beans.GenericTypeAwarePropertyDescriptor] - [Invalid JavaBean property 'credentials' being accessed! Ambiguous write methods found next to actually used [public void a.b.c.d.CustomHttpState.setCredentials(org.apache.commons.httpclient.Credentials)]: ...(error continues)
. Это обычно?4. Нет, это не обычно. Вы точно скопировали и вставили класс CustomHttpState?
5. Вы действительно должны указывать
requires-channel="https"
в каждой из ваших записей intercept-url. Вы никогда не должны отправлять информацию об имени пользователя / пароле по незашифрованному http.
Ответ №3:
RestTemplate очень простой и ограниченный; кажется, что нет простого способа сделать это. Вероятно, лучшим способом является реализация дайджеста базовой аутентификации в веб-приложении 1. Затем используйте Apache HttpClient напрямую для доступа к службам rest из веб-приложения 2.
При этом для тестирования я смог обойти это с помощью большого взлома. По сути, вы используете RestTemplate для отправки логина (j_spring_security_check), выделяете jsessionid из заголовков запроса, затем отправляете запрос rest. Вот код, но я сомневаюсь, что это лучшее решение для готового к производству кода.
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=useramp;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());
}
}
Обратите внимание, что для того, чтобы это сработало, вам необходимо создать хранилище доверия в JCA, чтобы действительно можно было установить SSL-соединение. Я предполагаю, что вы не хотите, чтобы логин Spring Security выполнялся по обычному HTTP для производственного сайта, поскольку это было бы огромной дырой в безопасности.
Комментарии:
1. я понимаю, что вы ответили на этот вопрос более года назад, но я хотел бы воспользоваться вашим предложением «опубликовать код», если он все еще доступен. 🙂 спасибо
2. Код опубликован, но я не пробовал его снова с последней версией Spring. Не был ли обновлен RestTemplate?
Ответ №4:
Есть простой способ сделать это на случай, если вы тот, кто ищет простой вызов, а не пользователь 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);
Комментарии:
1. Похоже, это настройка RequestFactory RestTemplate, что происходит, когда вы хотите получить доступ к нескольким URL, где для всех из них требуется разная аутентификация дайджеста. Вы можете добавить аутентификацию для каждого хоста / или вы должны использовать новый RestTemplate? Не могли бы вы привести пример
2. Я думаю, вам пришлось бы создать RestTemplate для каждого типа дайджеста, или вы могли бы расширить
CommonsClientHttpRequestFactory
и в зависимости от запроса определить, какой клиент использовать
Ответ №5:
Следующее будет аутентифицировать и возвращать файл cookie сеанса:
String sessionCookie= restTemplate.execute(uri, HttpMethod.POST, request -> {
request.getBody().write(("j_username=USER_NAMEamp;j_password=PASSWORD").getBytes());
}, response -> {
AbstractClientHttpResponse r = (AbstractClientHttpResponse) response;
HttpHeaders headers = r.getHeaders();
return headers.get("Set-Cookie").get(0);
});
Ответ №6:
Учетные данные пользователя, прошедшего проверку подлинности в данный момент, должны быть доступны в веб-приложении 1 для Authentication
объекта, который доступен через SecurityContext
(например, вы можете получить его, вызвав SecurityContextHolder.getContext().getAuthentication()
).
После получения учетных данных вы можете использовать их для доступа к Web App 2.
Вы можете передать заголовок «Аутентификация» с помощью RestTemplate, либо расширив его с помощью декоратора (как описано здесь), либо используя RestTemplate.exchange()
метод, как описано в этом сообщении на форуме.
Комментарии:
1. Я бы предположил, что веб-приложение 2 не может видеть HTTP-сеанс для веб-приложения 1, так что это, вероятно, не сработает.
2. Извините, неправильно понял желаемое направление: из веб-приложения 2 в веб-приложение 1. Меняю свой ответ.
Ответ №7:
Это очень похоже на подход ams, за исключением того, что я полностью инкапсулировал проблему сохранения cookie сеанса в StatefulClientHttpRequestFactory. Кроме того, украсив существующий 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<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();
}
}
}