복붙노트

[SPRING] RestTemplate을 이용한 Spring 보안 인증

SPRING

RestTemplate을 이용한 Spring 보안 인증

2 개의 별도 서비스 세트를 제공하는 2 개의 스프링 웹 앱이 있습니다. Web App 1에는 사용자 기반 인증을 사용하여 구현 된 Spring Security가 있습니다.

이제 Web App 2는 Web App 1의 서비스에 액세스해야합니다. 일반적으로 RestTemplate 클래스를 사용하여 다른 웹 서비스에 요청합니다.

Web App 2의 요청에서 Web App 1에 인증 자격 증명을 전달하는 방법

해결법

  1. ==============================

    1.나는 같은 상황에 처해 있었다. 여기에 내 해결책이있다.

    나는 같은 상황에 처해 있었다. 여기에 내 해결책이있다.

    서버 - 봄 보안 설정

    <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>
    
  2. ==============================

    2.다음은 Spring 3.1과 Apache HttpComponents 4.1에서 잘 작동하는 솔루션입니다.이 사이트에서 다양한 답변을 만들고 Spring RestTempalte 소스 코드를 읽었습니다. 나는 다른 사람들에게 시간을 절약하기를 희망하며 공유하고있다. 나는 스프링이 이와 같은 코드를 가지고 있어야한다고 생각하지만 그렇지 않다.

    다음은 Spring 3.1과 Apache HttpComponents 4.1에서 잘 작동하는 솔루션입니다.이 사이트에서 다양한 답변을 만들고 Spring RestTempalte 소스 코드를 읽었습니다. 나는 다른 사람들에게 시간을 절약하기를 희망하며 공유하고있다. 나는 스프링이 이와 같은 코드를 가지고 있어야한다고 생각하지만 그렇지 않다.

    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을 가진 모든 요청에서 동일하게 설정하는 Factory 클래스입니다.

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

    아래는 Stateful Resttemplate입니다. 쿠키를 기억하기 위해 사용할 수 있습니다. 일단 로그인하면 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;
        }
    }
    

    다음은 휴식 클라이언트를 나타내는 클래스로, 스프링으로 보안 된 앱으로 호출 할 수 있습니다. 보안.

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

    3.RestTemplate은 매우 기본적이고 제한적입니다. 이것을하는 쉬운 방법이없는 것 같습니다. 가장 좋은 방법은 Web App 1에서 기본 인증 다이제스트를 구현하는 것입니다. 그런 다음 Apache HttpClient를 직접 사용하여 Web App 2에서 나머지 서비스에 액세스합니다.

    RestTemplate은 매우 기본적이고 제한적입니다. 이것을하는 쉬운 방법이없는 것 같습니다. 가장 좋은 방법은 Web App 1에서 기본 인증 다이제스트를 구현하는 것입니다. 그런 다음 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());
    }
    

    }

    이것이 작동하려면 SSL 연결이 실제로 이루어 지도록 JCA에서 트러스트 스토어를 생성해야한다. 스프링 보안 계정의 로그인은 대량의 보안 허점이 될 수 있기 때문에 프로덕션 사이트의 일반 HTTP보다 길어지기를 원하지 않는다고 가정합니다.

  4. ==============================

    4.당신이 API 소비자가 아닌 단순한 호출을 찾고있는 누군가를 위해서라면 이것을 할 수있는 간단한 방법이 있습니다.

    당신이 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);
    
  5. ==============================

    5.다음은 세션 쿠키를 인증하고 반환합니다.

    다음은 세션 쿠키를 인증하고 반환합니다.

    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);
        });
    
  6. ==============================

    6.현재 인증 된 사용자 자격 증명은 SecurityContext를 통해 액세스 할 수있는 인증 객체의 Web App 1에서 사용할 수 있어야합니다 (예 : SecurityContextHolder.getContext (). getAuthentication ())을 호출하여 검색 할 수 있습니다.

    현재 인증 된 사용자 자격 증명은 SecurityContext를 통해 액세스 할 수있는 인증 객체의 Web App 1에서 사용할 수 있어야합니다 (예 : SecurityContextHolder.getContext (). getAuthentication ())을 호출하여 검색 할 수 있습니다.

    자격 증명을 검색 한 후에 자격 증명을 사용하여 Web App 2에 액세스 할 수 있습니다.

  7. ==============================

    7.이는 StatefulClientHttpRequestFactory에 세션 쿠키를 유지하는 것에 대한 우려를 완전히 캡슐화했다는 점을 제외하면 ams의 접근법과 매우 유사합니다. 또한이 동작으로 기존 ClientHttpRequestFactory를 꾸미면 모든 기본 ClientHttpRequestFactory와 함께 사용할 수 있으며 특정 구현에 바인딩되지 않습니다.

    이는 StatefulClientHttpRequestFactory에 세션 쿠키를 유지하는 것에 대한 우려를 완전히 캡슐화했다는 점을 제외하면 ams의 접근법과 매우 유사합니다. 또한이 동작으로 기존 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();
            }
        }
    }
    
  8. from https://stackoverflow.com/questions/4615039/spring-security-authentication-using-resttemplate by cc-by-sa and MIT license