preface

Recently, the project was connected to a CAS-based system for single sign-on (SSO). I had done CAS connection in the traditional framework before, and it was supposed to be easy, just configuring the accounts of both parties, but now the system architecture is adjusted to SpringCloud micro-service system, which makes the connection not so simple and direct. This paper mainly discusses the combination of CAS and Zuul.

The CAS theory

The basic principles of CAS are fairly simple, as shown below.



The main is

1. Intercept the redirection

2. Login

3. Verify

4. Obtain user information

It looks complicated, but the framework already does most of the work, encapsulates it, and normally we just need to configure it, but it can be confusing in a microservices environment.

By the way, we use the test system built by version CAS3.5.

Implementation approach

Zuul’s role as the gateway to the entire system is particularly suited to several tasks: 1. Routing, Zuul’s first job. Load balancing and Zuul 2.0 performance can be expected 3. Logging, which is important and necessary since external requests go through Zuul 4. Authentication, also because external services are through Zuul, authentication is also very appropriate, so for the SpringCloud system to do CAS single sign-on integration Zuul is the most appropriate.

Zuul is also SpringBoot based, so we can use Spring Security routines to implement CAS interception and verification. To summarize: 1. Integrate CAS on Zuul 2. Use the Spring Security package

But we also know that Zuul also handles logs, so we need to coordinate the responsibilities of CAS with Zuul itself. We also know that Zuul’s core is ZuulFilter, and SpringSecurity is essentially a series of filters to handle. Sorting out the two fillters is a prerequisite for solving the problem.

Let’s see how to solve this problem with specific code.

Show me the code.

The sample code

The first is the project directory, maven’s convention

Ps Never mind that UserLoginInfoCache. Java is actually a cache.

And then the POM. XML

<? The XML version = "1.0" encoding = "utf-8"? > < 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 > com. Zw. Se2 < / groupId > < artifactId > demo - zuul < / artifactId > <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name> Hy-Zuul </name> <description>Demo project for Zuul and  CAS</description> <parent> <groupId>org.springframework.boot</groupId> The < artifactId > spring - the boot - starter - parent < / artifactId > < version > 2.0.1. RELEASE < / version > < relativePath / > <! -- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> < project. Reporting. OutputEncoding > utf-8 < / project. Reporting. OutputEncoding > < Java version > 1.8 < / Java version > <spring-cloud.version>Finchley.RC1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <dependency> <groupId>org.springframework.session</groupId> < artifactId > spring - the session < / artifactId > < version > 1.3.1. The RELEASE < / version > < / dependency > < the dependency > <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </project>Copy the code

Next comes the Zuul configuration application.properties

By default, sensitive headers cannot be passed through the API gateway. Zuul.routes.tim-service.sensitiveheaders ="*" zuul.routes.main-service.path=/main-service/** zuul.routes.main-service.url=http://localhost:8383 zuul.routes.main-service.sensitiveHeaders="*" devMode=false spring.application.name=demo-zuul-server # zuul-prefix = zuul-prefix = zuul-prefix Zuul.strip -prefix=false server.port=8085 # Disable hystrix timeout Hystrix.com mand. Default. Execution. A timeout. Enabled = false # session storage spring session. The store -type = none # log configuration file path Logging. Config = ext/conf/logback. XML # CAS service address CAS. The server host. Url = http://10.0.4.53:8080/cas-server-webapp-3.5.0 Cas.server.host. login_url=${cas.server.host.url}/login # App.server.host. url=http://localhost:8085 App.login. URL =/cas/login/zuulCopy the code

The next step is to start the configuration bootstrap.yaml

eureka:
  client:
    service-url:
        defaultZone: http://localhost:8797/eureka
spring:
  cloud:
    config:
      uri: http://localhost:8888
      profile: dev
      name: hyConfig
Copy the code

Next, cas casproperties.java

package com.zw.se2.hy.zuul.cas.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * Created by ZEW on 2018/6/7.
 */
@Data
@Component
public class CasProperties {
    @Value("${cas.server.host.url}")
    private String casServerUrl;

    @Value("${cas.server.host.login_url}")
    private String casServerLoginUrl;

    @Value("${app.server.host.url}")
    private String appServerUrl;

    @Value("${app.login.url}")
    private String appLoginUrl;

}
Copy the code

Securityconfig.java, which is the core of the configuration, the core of which is configuring the interception policy and processing filters.

package com.zw.se2.hy.zuul.cas.config; import com.zw.se2.hy.zuul.cas.custom.CustomUserDetailsService; import org.jasig.cas.client.session.SingleSignOutFilter; import org.jasig.cas.client.validation.Cas20ServiceTicketValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.cas.ServiceProperties; import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; import org.springframework.security.cas.authentication.CasAuthenticationProvider; import org.springframework.security.cas.web.CasAuthenticationEntryPoint; import org.springframework.security.cas.web.CasAuthenticationFilter; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; /** * Created by ZEW on 2018/6/7. */ @configuration @enableWebSecurity // EnableWeb permissions @ EnableGlobalMethodSecurity (prePostEnabled = true) / / enable the methods validation public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CasProperties casProperties; /** Define the source of authentication user information, Password validation rules * / @ Override protected void the configure (AuthenticationManagerBuilder auth) throws the Exception { super.configure(auth); auth.authenticationProvider(casAuthenticationProvider()); } /** Define security policies */ @override protected void configure(HttpSecurity HTTP) throws Exception {http.authorizerequests ()// Configure security policies .antMatchers("/**/api/**").permitAll() .antMatchers("/**/**.html").permitAll() .anyRequest().authenticated(); / / definition/request. Do not need to verify the HTTP exceptionHandling () authenticationEntryPoint (casAuthenticationEntryPoint ()). And () .addFilter(casAuthenticationFilter()) .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class); Http.csrf ().disable(); http.csrf(); http.csrf(); } / certification entry * / * * @ Bean public CasAuthenticationEntryPoint CasAuthenticationEntryPoint () {CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint(); casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl()); casAuthenticationEntryPoint.setServiceProperties(serviceProperties()); return casAuthenticationEntryPoint; } @bean public ServiceProperties ServiceProperties () {ServiceProperties ServiceProperties = new ServiceProperties(); serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl()); serviceProperties.setAuthenticateAllArtifacts(true); return serviceProperties; } /**CAS authentication filter */ @bean public CasAuthenticationFilter CasAuthenticationFilter () throws Exception { CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); casAuthenticationFilter.setAuthenticationManager(authenticationManager()); casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl()); return casAuthenticationFilter; } / cas authentication Provider * / * * @ Bean public CasAuthenticationProvider CasAuthenticationProvider () {CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider(); // This implements a custom authentication service, There is no special requirements for can use the default service casAuthenticationProvider. SetAuthenticationUserDetailsService (customUserDetailsService ()); casAuthenticationProvider.setServiceProperties(serviceProperties()); casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator()); casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only"); return casAuthenticationProvider; } /*@Bean public UserDetailsService customUserDetailsService(){ return new CustomUserDetailsService(); } * / / * * user-defined AuthenticationUserDetailsService * / @ Bean public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService(){ return new CustomUserDetailsService(); } @Bean public Cas20ServiceTicketValidator cas20ServiceTicketValidator() { return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl()); }}Copy the code

If you are interested, please refer to the reference link. However, please note that this document will not pass the authentication unless you assign the userInfo permission information in the loadUserDetails function. The code is as follows:

 UserInfo userInfo = new UserInfo();
        userInfo.setUsername(token.getName());
        userInfo.setName(token.getName());
        Set<AuthorityInfo> authorities = new HashSet<>();
        AuthorityInfo authorityInfo = new AuthorityInfo("CAS");
        authorities.add(authorityInfo);
        userInfo.setAuthorities(authorities);
        userInfo.setAccountNonLocked(true);
        userInfo.setAccountNonExpired(true);
        userInfo.setCredentialsNonExpired(true);
Copy the code

After the integration of CAS, Zuul’s own filter has been tested and the CAS filter has a higher priority than Zuul’s Pre filter, which is also convenient, and only needs to deal with logins and other logs. Add a POST filter. As shown in the following LoginResponseFilter. Java.

package com.zw.se2.hy.zuul.filter.post; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.zw.se2.hy.zuul.UserLoginInfoCache; import com.zw.se2.hy.zuul.filter.ConstantPath; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import org.springframework.web.client.RestTemplate; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import static org.springframework.util.ReflectionUtils.rethrowRuntimeException; @Component public class LoginResponseFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger("monitor"); @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); String url = context.getRequest().getRequestURL().toString(); Return stringutils. endsWith(URL, constantpath.login_path); } @Override public Object run() { try { RequestContext context = RequestContext.getCurrentContext(); InputStream stream = context.getResponseDataStream(); String body = StreamUtils.copyToString(stream, Charset.forName("UTF-8")); String url = context.getRequest().getRequestURL().toString(); If (stringutils.isnotBlank (body)) {// Verify that the response result is login successfully JSONObject bodyJson = jsonObject.fromobject (body); if (bodyJson.has(ConstantPath.LOGIN_RESPONSE_STATUS)) { String status = bodyJson.getString(ConstantPath.LOGIN_RESPONSE_STATUS); if (StringUtils.equals(status, "200")) { if (bodyJson.has(ConstantPath.LOGIN_RESPONSE_RESULT)) { JSONArray resultArray = bodyJson.getJSONArray(ConstantPath.LOGIN_RESPONSE_RESULT); if (resultArray ! = null && resultArray.size() > 0) { JSONObject userObject = resultArray.getJSONObject(0); processLogin(context, userObject); } } } } } context.setResponseBody(body); } catch (IOException e) { rethrowRuntimeException(e); } return null; } private void processLogin(RequestContext context, JSONObject userObject) { if (userObject.has(ConstantPath.LOGIN_USERNAME)) { String userName = userObject.getString(ConstantPath.LOGIN_USERNAME); HttpServletRequest request = context.getrequest (); HttpSession session = request.getSession(); session.setAttribute("userName", userName); session.setMaxInactiveInterval(1800); The info (" > > > user > > > "+ userName +" carried out > > > login > > > operation); } } @Override public String filterType() { return "post"; } @Override public int filterOrder() { return 1; }}Copy the code

conclusion

As you can see Zuul and Cas are pretty easy to combine as long as you clear your head, the key is to clear your head.