使用Spring Integration with CAS时如何访问安全的后端服务?

时间:2016-12-02 10:49:30

标签: spring-security spring-integration cas microservices

我正在寻求将当前整体系统迁移到微服务架构的解决方案。我想使用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应用程序。我在同一个本地Tomcat(版本7.0.36)上部署了三个war文件,只需将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的原因。我做错了什么或错过了一些配置吗?请分享您的想法,意见和建议。提前谢谢。

1 个答案:

答案 0 :(得分:0)

我之前从未使用过CAS,但看起来你不赞同如何获得headers.serviceTicket

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

  

成功登录后,CAS会将用户的浏览器重定向回原始服务。它还包括一个ticket参数,它是一个表示“服务票证”的不透明字符串。继续我们之前的示例,浏览器重定向到的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,而凭证将是服务票不透明值......

因此,看起来我们不需要担心<int-http:inbound-channel-adapter>中的请求参数,只需依赖SecurityContext中的<int-http:outbound-gateway>

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