【问题标题】:Spring Security Authentication using RestTemplate使用 RestTemplate 的 Spring Security 身份验证
【发布时间】:2011-06-04 15:25:07
【问题描述】:

我有 2 个 Spring Web 应用程序,它们提供 2 组独立的服务。 Web App 1 使用基于用户的身份验证实现了 Spring Security。

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

我们如何将Web App 2请求中的认证凭证传递给Web App 1

【问题讨论】:

    标签: java spring authentication spring-security


    【解决方案1】:

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

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

    您可以通过使用装饰器(如here 所述)或使用RestTemplate.exchange() 方法(如in this forum post 所述)扩展RestTemplate 来传递“Authentiation”标头。

    【讨论】:

    • 我假设 Web App 2 看不到 Web App 1 的 HTTP 会话,所以这可能行不通。
    • 抱歉,误解了期望的方向:from Web App2 to Web App 1. 更改我的答案。
    【解决方案2】:

    RestTemplate 非常基本且有限;似乎没有一种简单的方法可以做到这一点。最好的方法可能是在Web App 1中实现basic auth的digest。然后直接使用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<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 用于生产站点,因为这将是一个巨大的安全漏洞。

    【讨论】:

    • 我知道您在一年前回答了这个问题,但如果它仍然可用,我愿意接受您“发布代码”的提议。 :) 谢谢
    • 已发布代码,但尚未使用最新版本的 Spring 再次尝试。 RestTemplate 没有更新吗?
    【解决方案3】:

    我也遇到了同样的情况。这是我的解决方案。

    服务器 - 春季安全配置

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

    【讨论】:

    • 您能解释一下为什么需要自定义 http 状态类吗?
    • @kamaci HttpState#setCredentials 不是设置器(也称为访问器),因为需要两个参数。所以凭证不是 POJO 字段,不能在 Spring xml 配置中访问。
    • 当我运行我的应用程序时,它会记录:[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)。平时吗?
    • 不,这不常见。您是否完全复制并粘贴了 CustomHttpState 类?
    • 您确实应该在每个拦截 URL 条目中指定 requires-channel="https"。永远不要通过未加密的 http 发送用户名/密码信息。
    【解决方案4】:

    这是一个非常适用于 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;
        }
    }
    

    下面是 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;
        }
    }
    

    这是一个代表休息客户端的类,以便您可以调用受 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();
        }
    }
    

    【讨论】:

    • 对于那些想要在 Android SDK 23+ 中使用此代码的用户,请在您的模块 gradle 文件中添加 gradle 依赖"org.apache.httpcomponents:httpcore:4.3.2"。
    【解决方案5】:

    如果您是寻找简单调用而不是 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);
    

    【讨论】:

    • 这似乎是在配置 RestTemplate RequestFactory,当您想要访问多个 url 时会发生什么,这些 url 都需要不同的摘要身份验证。您可以添加每个主机身份验证/或者您必须使用新的 restTemplate ?你能举个例子吗
    • 我猜您必须为每种摘要类型创建一个 restTemplate,或者您可以扩展 CommonsClientHttpRequestFactory 并根据请求确定使用哪个客户端
    【解决方案6】:

    以下将验证并返回会话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);
        });
    

    【讨论】:

      【解决方案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();
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2020-12-10
        • 2011-11-14
        • 2016-09-09
        • 2014-03-26
        • 2022-01-15
        • 2014-02-26
        • 1970-01-01
        • 2012-11-27
        相关资源
        最近更新 更多