【问题标题】:How to access secured backend services when using Spring Integration with CAS?使用 Spring 与 CAS 集成时如何访问安全的后端服务?
【发布时间】:2017-04-17 06:13:31
【问题描述】:

我正在寻找将当前整体系统迁移到微服务架构的解决方案。我想使用 Spring Integration 和 Spring Security 来集成和保护服务。据我了解,保护后端服务更像是单点登录(SSO)。我使用 Jasig CAS 4.2.7(似乎与 Spring Security 配合良好)来集中验证用户,Spring Integration 4.2.11.RELEASE 和 Spring Security 4.0.4.RELEASE。

我创建了一个 Maven 项目,其中包含两个名为 web 和 service 的模块,它们都是 Web 应用程序。我将三个war文件部署在同一个本地Tomcat(版本7.0.36)上,只需将jimi和bob添加到CAS属性文件中,以确保它们通过CAS的身份验证。当我尝试访问 URL http://localhost:8080/prototype-integration-security-web/user 时,我通过了前端应用程序的身份验证,但在后端服务上禁止访问。

POM 文件如下所示。

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>prototype.integration.security</groupId>
  <artifactId>prototype-integration-security</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>prototype-integration-security</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <warName>${project.name}</warName>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-http</artifactId>
        <version>4.2.11.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.2.7.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-jcl</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>7.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-security</artifactId>
        <version>4.2.11.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-cas</artifactId>
        <version>4.0.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.4</version>
    </dependency>
  </dependencies>
  <modules>
    <module>prototype-integration-security-web</module>
    <module>prototype-integration-security-service</module>
  </modules>
</project>

两个模块的部署描述文件web.xml看起来一样,只是显示名称如下。

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                             http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="IntegrationSecurityWeb" version="3.0">
  <display-name>Integration Security Web Prototype</display-name>

  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

在web模块的Spring应用上下文配置文件中,dispatcher-servlet.xml如下所示。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:int-http="http://www.springframework.org/schema/integration/http"
       xmlns:int-security="http://www.springframework.org/schema/integration/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/task
                           http://www.springframework.org/schema/task/spring-task.xsd
                           http://www.springframework.org/schema/security
                           http://www.springframework.org/schema/security/spring-security.xsd
                           http://www.springframework.org/schema/integration
                           http://www.springframework.org/schema/integration/spring-integration-4.2.xsd
                           http://www.springframework.org/schema/integration/http
                           http://www.springframework.org/schema/integration/http/spring-integration-http-4.2.xsd
                           http://www.springframework.org/schema/integration/security
                           http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd">

  <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
      <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <constructor-arg>
          <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
            <property name="targetClass" value="org.apache.http.impl.client.HttpClients"/>
            <property name="targetMethod" value="createMinimal"/>
          </bean>
        </constructor-arg>
      </bean>
    </constructor-arg>
    <property name="messageConverters">
      <list>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.FormHttpMessageConverter">
        </bean>
      </list>
    </property>
  </bean>

  <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/prototype-integration-security-web/login/cas" />
    <property name="sendRenew" value="false" />
  </bean>

  <!-- Access voters -->
  <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
    <constructor-arg name="decisionVoters">
      <list>
        <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
          <constructor-arg>
            <bean class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
              <property name="hierarchy">
                <value>
                  ROLE_ADMIN > ROLE_USER
                </value>
              </property>
            </bean>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
      </list>
    </constructor-arg>
  </bean>

  <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <property name="loginUrl" value="https://localhost:8443/cas/login" />
    <property name="serviceProperties" ref="serviceProperties" />
  </bean>

  <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
  </bean>

  <!-- This filter handles a Single Logout Request from the CAS Server -->
  <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

  <!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
  <bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <constructor-arg value="http://localhost:8080/cas/logout" />
    <constructor-arg>
      <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
    </constructor-arg>
    <property name="filterProcessesUrl" value="/logout/cas" />
  </bean>

  <security:http entry-point-ref="casEntryPoint" access-decision-manager-ref="accessDecisionManager" use-expressions="false">
    <security:intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
    <security:intercept-url pattern="/**" access="ROLE_USER" />
    <security:form-login />
    <security:logout />
    <security:custom-filter before="LOGOUT_FILTER" ref="requestSingleLogoutFilter"/>
    <security:custom-filter before="CAS_FILTER" ref="singleLogoutFilter"/>
    <security:custom-filter position="CAS_FILTER" ref="casFilter" />
  </security:http>

  <security:user-service id="userService">
    <security:user name="jimi" password="jimi" authorities="ROLE_ADMIN" />
    <security:user name="bob" password="bob" authorities="ROLE_USER" />
  </security:user-service>

  <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <property name="authenticationUserDetailsService">
      <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <constructor-arg index="0" ref="userService" />
      </bean>
    </property>
    <property name="serviceProperties" ref="serviceProperties" />
    <property name="ticketValidator">
      <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
        <constructor-arg index="0" value="https://localhost:8443/cas" />
      </bean>
    </property>
    <property name="key" value="localCAS" />
  </bean>

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="casAuthenticationProvider" />
  </security:authentication-manager>

  <int:channel-interceptor order="99">
    <bean class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"/>
  </int:channel-interceptor>

  <task:executor id="pool" pool-size="5"/>

  <int:poller id="poller" default="true" fixed-rate="1000"/>

  <int-security:secured-channels>
    <int-security:access-policy pattern="user*" send-access="ROLE_USER" />
    <int-security:access-policy pattern="admin*" send-access="ROLE_ADMIN" />
  </int-security:secured-channels>

  <int-http:inbound-channel-adapter path="/user*" supported-methods="GET, POST" channel="userRequestChannel" />

  <int:channel id="userRequestChannel">
    <int:queue/>
  </int:channel>

  <int-http:outbound-channel-adapter url="http://localhost:8080/prototype-integration-security-service/query?ticket={ticket}"
                                     http-method="GET"
                                     rest-template="restTemplate"
                                     channel="userRequestChannel">
    <int-http:uri-variable name="ticket" expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials"/>
  </int-http:outbound-channel-adapter>

  <int-http:inbound-channel-adapter path="/admin/callback*"
                                    supported-methods="GET, POST"
                                    channel="adminRequestChannel" />

  <int:channel id="adminRequestChannel">
    <int:queue/>
  </int:channel>

  <int:logging-channel-adapter id="logging" channel="adminRequestChannel" level="DEBUG" />
</beans>

在服务模块的上下文配置文件中,dispatcher-servlet.xml如下所示。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:int-http="http://www.springframework.org/schema/integration/http"
       xmlns:int-security="http://www.springframework.org/schema/integration/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/task
                           http://www.springframework.org/schema/task/spring-task.xsd
                           http://www.springframework.org/schema/security
                           http://www.springframework.org/schema/security/spring-security.xsd
                           http://www.springframework.org/schema/integration
                           http://www.springframework.org/schema/integration/spring-integration-4.2.xsd
                           http://www.springframework.org/schema/integration/http
                           http://www.springframework.org/schema/integration/http/spring-integration-http-4.2.xsd
                           http://www.springframework.org/schema/integration/security
                           http://www.springframework.org/schema/integration/security/spring-integration-security-4.2.xsd">

  <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
      <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <constructor-arg>
          <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
            <property name="targetClass" value="org.apache.http.impl.client.HttpClients"/>
            <property name="targetMethod" value="createMinimal"/>
          </bean>
        </constructor-arg>
      </bean>
    </constructor-arg>
    <property name="messageConverters">
      <list>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.FormHttpMessageConverter">
        </bean>
      </list>
    </property>
  </bean>

  <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/prototype-integration-security-service/login/cas" />
    <property name="sendRenew" value="false" />
  </bean>

  <!-- Access voters -->
  <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
    <constructor-arg name="decisionVoters">
      <list>
        <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
          <constructor-arg>
            <bean class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
              <property name="hierarchy">
                <value>
                  ROLE_ADMIN > ROLE_USER
                </value>
              </property>
            </bean>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
        <!-- <bean class="org.springframework.security.web.access.expression.WebExpressionVoter" /> -->
      </list>
    </constructor-arg>
  </bean>

  <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <property name="loginUrl" value="https://localhost:8443/cas/login" />
    <property name="serviceProperties" ref="serviceProperties" />
  </bean>

  <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
  </bean>

  <!-- This filter handles a Single Logout Request from the CAS Server -->
  <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

  <!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
  <bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <constructor-arg value="https://localhost:8443/cas/logout" />
    <constructor-arg>
      <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
    </constructor-arg>
    <property name="filterProcessesUrl" value="/logout/cas" />
  </bean>

  <security:http entry-point-ref="casEntryPoint" access-decision-manager-ref="accessDecisionManager" use-expressions="false">
    <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
    <security:form-login />
    <security:logout />
    <security:custom-filter before="LOGOUT_FILTER" ref="requestSingleLogoutFilter"/>
    <security:custom-filter before="CAS_FILTER" ref="singleLogoutFilter"/>
    <security:custom-filter position="CAS_FILTER" ref="casFilter" />
  </security:http>

  <security:user-service id="userService">
    <security:user name="jimi" password="jimi" authorities="ROLE_ADMIN" />
    <security:user name="bob" password="bob" authorities="ROLE_USER" />
  </security:user-service>

  <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <property name="authenticationUserDetailsService">
      <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <constructor-arg index="0" ref="userService" />
      </bean>
    </property>
    <property name="serviceProperties" ref="serviceProperties" />
    <property name="ticketValidator">
      <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
        <constructor-arg index="0" value="https://localhost:8443/cas" />
      </bean>
    </property>
    <property name="key" value="localCAS" />
  </bean>

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider ref="casAuthenticationProvider" />
  </security:authentication-manager>

  <int:channel-interceptor order="99">
    <bean class="org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor"/>
  </int:channel-interceptor>

  <task:executor id="pool" pool-size="5"/>

  <int:poller id="poller" default="true" fixed-rate="1000"/>

  <int-security:secured-channels>
    <int-security:access-policy pattern=".*" send-access="ROLE_ADMIN" />
  </int-security:secured-channels>

  <int-http:inbound-channel-adapter path="/query*" supported-methods="GET, POST" channel="requestChannel" />

  <int:channel id="requestChannel">
    <int:queue/>
  </int:channel>

  <int-http:outbound-channel-adapter url="http://localhost:8080/prototype-integration-security-web/admin/callback?ticket={ticket}"
                                     http-method="GET"
                                     rest-template="restTemplate"
                                     channel="requestChannel">
    <int-http:uri-variable name="ticket" expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials" />
  </int-http:outbound-channel-adapter>
</beans>

不需要额外的代码,这就是我喜欢 Spring Integration 的原因。我做错了什么或错过了一些配置吗?请分享您的想法、意见和建议。提前致谢。

【问题讨论】:

    标签: spring-security spring-integration cas microservices


    【解决方案1】:

    我以前从未使用过 CAS,但看起来你没有分享你是如何获得 headers.serviceTicket 的。

    我认为你的想法是通过 URL 参数传播 ticket 很好,但首先我们必须从传入的 URL 中提取它:

    成功登录后,CAS 会将用户的浏览器重定向回原来的服务。它还将包括一个票据参数,它是一个表示“服务票据”的不透明字符串。继续我们之前的示例,浏览器重定向到的 URL 可能是 https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ

    http://docs.spring.io/spring-security/site/docs/4.2.0.RELEASE/reference/htmlsingle/#cas

    为此,我们可以这样做:

    <int-http:inbound-channel-adapter path="/user*" supported-methods="GET, POST" channel="userRequestChannel">
           <int-http:header name="serviceTicket" expression="#requestParams.ticket"/>
    </int-http:inbound-channel-adapter>
    

    否则,请分享有关此事的异常情况并尝试跟踪网络流量以确定差距。

    更新

    根据 CAS 的 Spring Security 页面上的描述,我们有:

    ...主体将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭据将是服务票证不透明值...

    所以,看起来我们不需要担心&lt;int-http:inbound-channel-adapter&gt; 中的请求参数,只需依赖&lt;int-http:outbound-gateway&gt; 中的SecurityContext

    <int-http:uri-variable name="ticket" 
             expression="T(org.springframework.security.core.context.SecurityContextHolder).context.authentication.credentials"/>
    

    【讨论】:

    • 感谢您的建议。我尝试如下,但没有运气得到一个例外,说票不是 MultipleValueMap 的公共票,我猜这意味着票键值对不存在。 &lt;int-http:inbound-channel-adapter path="/user*" supported-methods="GET, POST" channel="userRequestChannel"&gt; &lt;int-http:header name="serviceTicket" expression="#requestParams.ticket.get(0)"/&gt; &lt;/int-http:inbound-channel-adapter&gt; 也许 CasAuthenticationFilter 已经删除了票,我必须实现自己的自定义过滤器才能在某处持有票。
    • 请在我的回答中找到更新。
    • 嗨,Artem,非常感谢您的建议,现在我可以从 SecurityContextHolder 获得服务票证。但是它还不起作用,我的访问仍然被 403 错误拒绝。尽管我尝试通过设置 outbound-gateway 的 transfer-cookie="true" 来替换 web 模块的原始 outbound-channel-adapter 来传输 cookie,但后端服务似乎根本没有验证票证。
    • 我还尝试直接在我的网络浏览器中访问网址 http://localhost:8080/prototype-integration-security-service/query?ticket=ST-3-e7vm3Jh13VeBKeLtmzL9-localhosthttp://localhost:8080/prototype-integration-security-web/admin/callback,它们都返回代码 200。也许 restTemplate 没有完全遵循 HTTP 重定向结构。我再次谷歌,发现 [stackoverflow.com/questions/32392634/… 谈论重定向然后我将出站网关更改为使用 HTTP GET 方法,然后一切顺利。
    • 对于 HTTP POST 方法,我需要更深入地挖掘 HttpComponentsClientHttpRequestFactory。我认为这个原型到目前为止是可行的。
    猜你喜欢
    • 2021-01-17
    • 2012-08-28
    • 1970-01-01
    • 2020-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-22
    • 1970-01-01
    相关资源
    最近更新 更多