如何使用 Spring Security OAuth2 和 MITREID Connect Introspect 保护资源?

How to Protect a Resource using Spring Security OAuth2 and MITREID Connect Introspect?

我们正在构建一个 REST 资源服务器(一个 Java 示例应用程序),我们计划使用 MITREID Connect 项目提供的 RFC7662 定义的身份传播机制来保护它。我们测试了两种配置方法,XML 设置,以及添加到资源服务器 class 的基于注解的设置(参见下面附带的示例代码)。

我们的测试显示 Spring 安全例程的初始化成功,但我们没有成功触发 Bearer 令牌通过授权 header。请求和资源执行成功,但没有进行令牌解析和内省验证。请检查下面随附的配置设置和日志。

欢迎支持以隔离组件之间丢失的线路(Spring 安全,Spring Oauth2 和 Mitreid Connect Introspect)。

安装文件:spring-security.xml

<?xml version="1.0" encoding="UTF-8"?>

http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.1.xsd">

<sec:http pattern="/css/**" security="none" />
<sec:http pattern="/js/**" security="none" />

<sec:http auto-config="true" use-expressions="true"
    disable-url-rewriting="true" entry-point-ref="oauthAuthenticationEntryPoint"
    pattern="/rest/service/sample/restService">

    <sec:custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter" />
</sec:http>

<sec:authentication-manager alias="authenticationManager">
</sec:authentication-manager>

<!-- Begin OAuth2 Introspect configuration -->


<bean id="oauthAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="W3IDRealm" />
</bean>

<oauth:resource-server id="resourceServerFilter"
    token-services-ref="introspectingService" />

<bean id="introspectingService"
    class="org.mitre.oauth2.introspectingfilter.IntrospectingTokenService">
    <property name="introspectionConfigurationService" ref="staticIntrospectionConfigurationService">
    </property>
</bean>

    <!-- <oauth:resource 
    id="protectedResource" 
    access-token-uri="${oidc.tokenEndpointUri}" 
    client-secret="${oidc.clientSecret}" 
    client-id="${oidc.clientId}"></oauth:resource> -->

<bean
    class="org.mitre.oauth2.introspectingfilter.service.impl.StaticIntrospectionConfigurationService"
    id="staticIntrospectionConfigurationService">
    <property name="introspectionUrl" value="${oidc.introspectEndpointUri}" />
    <property name="clientConfiguration">
        <bean class="org.mitre.oauth2.model.RegisteredClient">
            <property name="clientId" value="${oidc.clientId}" />
            <property name="clientSecret" value="${oidc.clientSecret}" />
        </bean>
    </property>
    <!-- <property name="introspectionAuthorityGranter">
        <bean class="org.mitre.oauth2.introspectingfilter.SimpleIntrospectionAuthorityGranter">
            <property name="authorities">
                <value>ROLE_API</value>
            </property>
        </bean>
    </property> -->
</bean>

resource.java

    package com.red.sampleoidcservice;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@EnableWebSecurity
@Configuration
@EnableResourceServer
public class RestController {

    private static final Logger logger = LoggerFactory.getLogger(RestController.class);

    @RequestMapping(value = "/restService", method = RequestMethod.POST)
    public @ResponseBody String restService(HttpServletRequest request, HttpServletResponse respose) {

        logger.info("Calling rest service");

        String requestToString = request.toString();

        String headerType = request.getHeader("Content-Type");
        String headerAuth = request.getHeader("Authorization");

        Map map = request.getParameterMap();

        String attributes = request.getAttributeNames().toString();

        // String someParam = request.getParameter("someParam");

        return "{\"status\":\"OK\"}";
    }

    protected static class ResourceServer extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.requestMatchers().antMatchers("/rest/service/sample/restService").and().authorizeRequests()
                    .anyRequest().access("#oauth2.hasScope('read')");
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("W3IDRealm");
        }
    }

}

post.java

    // HTTP POST request
private void sendPost(String token) throws Exception {

    try {

        token = "blablabla";

        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }

            public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }

            public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }
        } };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

        CloseableHttpClient httpClient = HttpClientBuilder.create().build();

        SSLConnectionSocketFactory f = new SSLConnectionSocketFactory(sc, new String[] { "TLSv1.2" }, null,
                org.apache.http.conn.ssl.SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);

        httpClient = HttpClients.custom().setSSLSocketFactory(f).build();

        HttpPost postRequest = new HttpPost("https://localhost:9444/rest/service/sample/restService");

        postRequest.addHeader("Content-Type", "application/x-www-form-urlencoded");

        List<NameValuePair> formparams = new ArrayList<NameValuePair>();
        formparams.add(new BasicNameValuePair("client_id", clientId));
        formparams.add(new BasicNameValuePair("client_secret", clientSecret));
        formparams.add(new BasicNameValuePair("token", token));

        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "utf-8");
        postRequest.setEntity(entity);

        postRequest.setHeader("Authorization", "Bearer " + token + "");

        HttpResponse response = httpClient.execute(postRequest, new BasicHttpContext());

        int statusCode = response.getStatusLine().getStatusCode();

        logger.info("HTTP status code : " + statusCode);

    } catch (Exception e) {
        logger.error(e.getMessage());
    }
}

跟踪:

INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization completed in 5872 ms

调试:org.springframework.web.servlet.DispatcherServlet - Servlet 'appServlet' 配置成功 调试:org.springframework.web.context.support.StandardServletEnvironment - 添加具有最低搜索优先级的 [servletConfigInitParams] PropertySource 调试:org.springframework.web.context.support.StandardServletEnvironment - 添加具有最低搜索优先级的 [servletContextInitParams] PropertySource 调试:org.springframework.web.context.support.StandardServletEnvironment - 添加具有最低搜索优先级的 [jndiProperties] PropertySource 调试:org.springframework.web.context.support.StandardServletEnvironment - 添加具有最低搜索优先级的 [systemProperties] PropertySource 调试:org.springframework.web.context.support.StandardServletEnvironment - 添加具有最低搜索优先级的 [systemEnvironment] PropertySource 调试:org.springframework.web.context.support.StandardServletEnvironment - 使用 PropertySources [servletConfigInitParams、servletContextInitParams、jndiProperties、systemProperties、systemEnvironment] 初始化 StandardServletEnvironment 调试:org.springframework.web.filter.DelegatingFilterProxy - 初始化过滤器 'springSecurityFilterChain' 调试:org.springframework.beans.factory.support.DefaultListableBeanFactory - 返回单例 bean 的缓存实例 'org.springframework.security.filterChainProxy' 调试:org.springframework.web.filter.DelegatingFilterProxy - 过滤器 'springSecurityFilterChain' 配置成功 调试:org.springframework.web.servlet.DispatcherServlet - 名称为 'appServlet' 的 DispatcherServlet 处理 POST 对 [/rest/service/sample/restService] 的请求 调试:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - 查找路径 /restService 的处理程序方法 调试:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - 返回处理程序方法 [public java.lang.String com.red.sampleoidcservice.RestController.restService(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)] 调试:org.springframework.beans.factory.support.DefaultListableBeanFactory - 返回单例 bean 的缓存实例 'restController' 信息:com.red.sampleoidcservice.RestController - 调用休息服务 调试:org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - 使用 [org.springframework.http.converter.StringHttpMessageConverter@6912d551] 将 [{"status":"OK"}] 编写为 "text/plain" DEBUG:org.springframework.web.servlet.DispatcherServlet - Null ModelAndView 返回给名称为 'appServlet' 的 DispatcherServlet:假设 HandlerAdapter 已完成请求处理 调试:org.springframework.web.servlet.DispatcherServlet - 成功完成请求 调试:org.springframework.beans.factory.support.DefaultListableBeanFactory - 返回单例 bean 的缓存实例 'delegatingApplicationListener'

找到解决方案

带注释的配置

    /*******************************************************************************
 * Copyright 2014 The MITRE Corporation
 *   and the MIT Kerberos and Internet Trust Consortium
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.RED.sampleoidcservice;

import java.io.IOException;
import java.security.Principal;
import java.util.Locale;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mitre.oauth2.introspectingfilter.IntrospectingTokenService;
import org.mitre.oauth2.introspectingfilter.service.impl.StaticIntrospectionConfigurationService;
import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
import org.mitre.oauth2.model.RegisteredClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.ModelAndView;

@Controller
@EnableWebSecurity
@Configuration
@EnableResourceServer // [2]
@ComponentScan({ "com.RED.sampleoidcservice" })
public class ResourceServer extends ResourceServerConfigurerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(ResourceServer.class);

    @Value("${oidc.jwks.keys}")
    private String jwksString;

    @Value("${oidc.introspectEndpointUri}")
    private String introspectURL;

    @Value("${oidc.clientId}")
    private String clientId;

    @Value("${oidc.clientSecret}")
    private String clientSecret;

    IntrospectingTokenService introspectTokenService = new IntrospectingTokenService();

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView modelHome(Locale locale, Principal p) {

        logger.info("Initializing service resource");

        ModelAndView model = new ModelAndView("/home.tiles");
        return model;
    }

    @RequestMapping(value = "/jwk", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody String jwk() {
        return jwksString;
    }

    @RequestMapping(value = "/restService", method = RequestMethod.POST)
    public @ResponseBody String restService(HttpServletRequest request, HttpServletResponse respose) {

        logger.info("Calling rest service");

        String requestToString = request.toString();

        String headerType = request.getHeader("Content-Type");
        String headerAuth = request.getHeader("Authorization");

        String token = headerAuth.split(" ")[1];

        // introspectTokenService.readAccessToken(token);

        Map map = request.getParameterMap();

        String attributes = request.getAttributeNames().toString();

        // String someParam = request.getParameter("someParam");

        return "{\"status\":\"OK\"}";
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
         http.requestMatcher(new OAuthRequestedMatcher())
         .authorizeRequests()
          .antMatchers(HttpMethod.OPTIONS).permitAll()
             .anyRequest().authenticated();
//      http.addFilterBefore(new TokenExtractorFilter(), BasicAuthenticationFilter.class).requestMatchers()
//              .antMatchers("/rest/service/sample/restService").and().authorizeRequests().anyRequest()
//              .access("ROLE_API");
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("W3IDRealm");
        resources.tokenExtractor(new BearerTokenExtractor());

        StaticIntrospectionConfigurationService introspectConfig = new StaticIntrospectionConfigurationService();

        introspectConfig.setIntrospectionUrl(introspectURL);

        RegisteredClient client = new RegisteredClient();
        client.setClientId(clientId);
        client.setClientSecret(clientSecret);
        client.setTokenEndpointAuthMethod(AuthMethod.NONE);

        introspectConfig.setClientConfiguration(client);

        introspectTokenService.setIntrospectionConfigurationService(introspectConfig);

        resources.tokenServices(introspectTokenService);
    }

    private static class OAuthRequestedMatcher implements RequestMatcher {

        public boolean matches(HttpServletRequest request) {

            String auth = request.getHeader("Authorization");
            // Determine if the client request contained an OAuth Authorization
            boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
            boolean haveAccessToken = request.getParameter("access_token")!=null;
            return haveOauth2Token || haveAccessToken;
        }

    }

    class TokenExtractorFilter extends OncePerRequestFilter implements Filter, InitializingBean {

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

            //UserDetails details = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

            BearerTokenExtractor bte = new BearerTokenExtractor();

            String mytoken = bte.extract(request).toString();

            logger.info("Filter activated");

        }

    }

}

我发布了一个使用基于注释的配置的解决方案,它帮助我们使用 Java 断点调试流程细节。使用 /introspect 的 OAuth2 Bearer 令牌验证现在正在运行。感谢 MITREID 提供的所有调试支持。请参阅报告最后一节中发布的代码解决方案,如上所示。