Bootstrap

自定义springboot starter-以cas-client为例

本文以编写spring-boot-cas-client为例,自定义一个starter;
实际项目过程中,推荐使用cas-client-autoconfig-support,该jar由第三方封装,功能较为全面;若与权限认证框架集成,可以选用相应的封装jar;shiro可选用pac4j,spring-security可选用spring-security-cas

<!-- https://mvnrepository.com/artifact/net.unicon.cas/cas-client-autoconfig-support -->
<dependency>
    <groupId>net.unicon.cas</groupId>
    <artifactId>cas-client-autoconfig-support</artifactId>
    <version>2.3.0-GA</version>
</dependency>

一.目录结构

D:\IDEA\uccp-spring-boot-starter>tree /F

D:.
│  pom.xml
└─src
    └─main
        ├─java
        │  └─com
        │      └─example
        │          └─uccp
        │              └─spring
        │                  └─boot
        │                      │  UccpAutoConfigure.java
        │                      │
        │                      └─autoconfigure
        │                          ├─filters
        │                          │      UccpFilterProxy.java
        │                          │      VirtualFilterChain.java
        │                          │
        │                          └─properties
        │                                  UccpProperties.java
        │
        └─resources
            └─META-INF
                    spring-configuration-metadata.json
                    spring.factories

二、代码

1.com.example.uccp.spring.boot.autoconfigure.properties.UccpProperties
package com.example.uccp.spring.boot.autoconfigure.properties;

import org.jasig.cas.client.Protocol;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author :ouruyi
 * @version 1.0
 * @date :Created in 2021/4/10 10:51
 * 功能描述:
 */
@ConfigurationProperties(prefix = "uccp")
public class UccpProperties {

    /**
     * 是否启用配置
     */
    private boolean enabled;
    /**
     * uccp-server地址
     * 示例:http://server.example.org:8800/uccp-server
     */
    private String casServerUrl;
    /**
     * uccp-service地址
     * 示例:http://service.example.org:8800/uccp-service
     */
    private String casServiceUrl;
    /**
     * cas client地址
     * 示例:http://client.example.org:8080
     */
    private String clientUrl;
    /**
     * uccp-service api接口调用code
     */
    private String appCode;
    /**
     * uccp-service api接口调用secret
     */
    private String appSecret;

    /**
     * 协议类型 {@link org.jasig.cas.client.Protocol}
     */
    private Protocol protocol;
    /**
     * 排除链接(多个用逗号分隔)
     */
    private String excludes;
    /**
     * filter拦截url
     */
    private String urlPatterns;

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getCasServerUrl() {
        return casServerUrl;
    }

    public void setCasServerUrl(String casServerUrl) {
        this.casServerUrl = casServerUrl;
    }

    public String getCasServiceUrl() {
        return casServiceUrl;
    }

    public void setCasServiceUrl(String casServiceUrl) {
        this.casServiceUrl = casServiceUrl;
    }

    public String getClientUrl() {
        return clientUrl;
    }

    public void setClientUrl(String clientUrl) {
        this.clientUrl = clientUrl;
    }

    public String getAppCode() {
        return appCode;
    }

    public void setAppCode(String appCode) {
        this.appCode = appCode;
    }

    public String getAppSecret() {
        return appSecret;
    }

    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }

    public Protocol getProtocol() {
        return protocol;
    }

    public void setProtocol(Protocol protocol) {
        this.protocol = protocol;
    }

    public String getExcludes() {
        return excludes;
    }

    public void setExcludes(String excludes) {
        this.excludes = excludes;
    }

    public String getUrlPatterns() {
        return urlPatterns;
    }

    public void setUrlPatterns(String urlPatterns) {
        this.urlPatterns = urlPatterns;
    }

    @Override
    public String toString() {
        return "UccpProperties{" +
                "enabled=" + enabled +
                ", casServerUrl='" + casServerUrl + '\'' +
                ", casServiceUrl='" + casServiceUrl + '\'' +
                ", clientUrl='" + clientUrl + '\'' +
                ", appCode='" + appCode + '\'' +
                ", appSecret='" + appSecret + '\'' +
                ", protocol=" + protocol +
                ", excludes='" + excludes + '\'' +
                ", urlPatterns='" + urlPatterns + '\'' +
                '}';
    }
}

2.com.example.uccp.spring.boot.UccpAutoConfigure
package com.example.uccp.spring.boot;

import cn.hutool.core.util.StrUtil;
import com.example.uccp.spring.boot.autoconfigure.filters.UccpFilterProxy;
import com.example.uccp.spring.boot.autoconfigure.properties.UccpProperties;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * @author :ouruyi
 * @version 1.0
 * @date :Created in 2021/4/10 10:36
 * 功能描述:自动配置类
 */
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(AbstractCasFilter.class)
@ConditionalOnProperty(prefix = "uccp", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(UccpProperties.class)
public class UccpAutoConfigure {

    public static final Logger logger = LoggerFactory.getLogger(UccpProperties.class);

    @Resource
    UccpProperties uccpProperties;

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new UccpFilterProxy(uccpProperties));
        filterRegistrationBean.addInitParameter("timeOut", "30");
        filterRegistrationBean.addInitParameter("excludes", uccpProperties.getExcludes());
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns(StrUtil.split(uccpProperties.getUrlPatterns(), ","));
        filterRegistrationBean.setOrder(10);
        return filterRegistrationBean;
    }

}

3.spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.uccp.spring.boot.UccpAutoConfigure
4.com.example.uccp.spring.boot.autoconfigure.filters.UccpFilterProxy
package com.example.uccp.spring.boot.autoconfigure.filters;

import cn.hutool.core.util.StrUtil;
import com.example.uccp.spring.boot.autoconfigure.properties.UccpProperties;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.util.AssertionThreadLocalFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author :ouruyi
 * @version 1.0
 * @date :Created in 2021/4/10 12:42
 * 功能描述:
 */
public class UccpFilterProxy implements Filter {

    public static final Logger logger = LoggerFactory.getLogger(UccpFilterProxy.class);

    private int sessionTimeOut = 30;

    private UccpProperties uccpProperties;

    private Filter[] filters;

    /**
     * 排除链接
     */
    public List<String> excludes = new ArrayList<>();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        String timeOut = filterConfig.getInitParameter("timeOut");

        final String excludesParam = filterConfig.getInitParameter("excludes");

        if (StrUtil.isNotBlank(excludesParam)) {
            String[] url = excludesParam.split(",");
            for (int i = 0; url != null && i < url.length; i++) {
                excludes.add(url[i]);
            }
        }

        try {
            this.sessionTimeOut = Integer.parseInt(timeOut);
        } catch (NumberFormatException e) {
            logger.debug("非法数值!{}", timeOut);
        }

        this.filters = this.obtainAllDefinedFilters();

        for (int i = 0; i < this.filters.length; ++i) {
            if (this.filters[i] != null) {
                logger.debug("filter {} is inited!", this.filters[i]);
                this.filters[i].init(filterConfig);
            }
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        HttpSession session = httpRequest.getSession(false);
        if (session != null && this.sessionTimeOut > 0) {
            session.setMaxInactiveInterval(this.sessionTimeOut * 60);
        }

        if (servletResponse instanceof HttpServletResponse) {
            ((HttpServletResponse) servletResponse).setHeader("P3P", "CP=CAO PSA OUR IDC DSP COR ADM DEVi TAIi PSD IVAi IVDi CONi HIS IND CNT");
        }

        if (!handleExcludeURL(httpRequest, httpResponse)) {
            if (this.filters != null && this.filters.length > 0) {
                VirtualFilterChain virtualFilterChain = new VirtualFilterChain(this.filters);
                virtualFilterChain.doFilter(servletRequest, servletResponse);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);

    }

    @Override
    public void destroy() {
        for (Filter filter : this.filters) {
            logger.debug("filter {} is destroyed!", filter);
            filter.destroy();
        }
    }


    private Filter[] obtainAllDefinedFilters() {
        Set<Filter> list = new LinkedHashSet();
        SingleSignOutFilter signOutFilter = new SingleSignOutFilter();
        signOutFilter.setCasServerUrlPrefix(uccpProperties.getCasServerUrl());
        signOutFilter.setIgnoreInitConfiguration(true);
        list.add(signOutFilter);

        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        StringBuilder loginUrl = new StringBuilder();
        loginUrl.append(uccpProperties.getCasServerUrl())
                .append("/login?appCode=")
                .append(uccpProperties.getAppCode());
        authenticationFilter.setCasServerLoginUrl(loginUrl.toString());
        authenticationFilter.setServerName(uccpProperties.getClientUrl());
        authenticationFilter.setIgnoreInitConfiguration(true);
        list.add(authenticationFilter);

        if (Protocol.CAS3 == uccpProperties.getProtocol()) {
            Cas30ProxyReceivingTicketValidationFilter validationFilter = new Cas30ProxyReceivingTicketValidationFilter();
            validationFilter.setUseSession(true);
            validationFilter.setServerName(uccpProperties.getClientUrl());
            validationFilter.setRedirectAfterValidation(true);
            validationFilter.setMillisBetweenCleanUps(60000);
            validationFilter.setIgnoreInitConfiguration(true);
            validationFilter.setTicketValidator(new Cas30ServiceTicketValidator(uccpProperties.getCasServerUrl()));
            list.add(validationFilter);
        } else {
            Cas20ProxyReceivingTicketValidationFilter validationFilter = new Cas20ProxyReceivingTicketValidationFilter();
            validationFilter.setUseSession(true);
            validationFilter.setServerName(uccpProperties.getClientUrl());
            validationFilter.setRedirectAfterValidation(true);
            validationFilter.setMillisBetweenCleanUps(60000);
            validationFilter.setIgnoreInitConfiguration(true);
            validationFilter.setTicketValidator(new Cas20ServiceTicketValidator(uccpProperties.getCasServerUrl()));
            list.add(validationFilter);
        }

        list.add(new AssertionThreadLocalFilter());

        list.add(new HttpServletRequestWrapperFilter());
        return list.toArray(new Filter[list.size()]);

    }

    /**
     * 直接放行url
     * @param request
     * @param response
     * @return
     */
    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
        if (excludes == null || excludes.isEmpty()) {
            return false;
        }
        String url = request.getServletPath();
        for (String pattern : excludes) {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(url);
            if (m.find()) {
                return true;
            }
        }
        return false;
    }

    public UccpFilterProxy(UccpProperties uccpProperties) {
        this.uccpProperties = uccpProperties;
    }

    public UccpFilterProxy() {

    }

    public UccpProperties getUccpProperties() {
        return uccpProperties;
    }

    public void setUccpProperties(UccpProperties uccpProperties) {
        this.uccpProperties = uccpProperties;
    }

}

5.com.example.uccp.spring.boot.autoconfigure.filters.VirtualFilterChain
package com.example.uccp.spring.boot.autoconfigure.filters;

import javax.servlet.*;
import java.io.IOException;

/**
 * @author :ouruyi
 * @version 1.0
 * @date :Created in 2021/4/13 17:26
 * 功能描述:自定义filterChain
 * @see org.apache.catalina.core.ApplicationFilterChain
 */
public class VirtualFilterChain implements FilterChain {

    private int i = 0;
    private Filter[] filters;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {

        int currentIndex = ++i - 1;
        if (currentIndex < filters.length) {
            filters[currentIndex].doFilter(servletRequest, servletResponse, this);
        }

    }

    public Filter[] getFilters() {
        return filters;
    }

    public void setFilters(Filter[] filters) {
        this.filters = filters;
    }

    public VirtualFilterChain(Filter[] filters) {
        this.filters = filters;
    }

    public VirtualFilterChain() {
    }
}

6.pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>uccp-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>uccp-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <cas.version>3.5.0</cas.version>
        <spring-boot.version>2.4.4</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- Import dependency management from Spring Boot,not include plugin
                    management as the parent import style -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jasig.cas.client</groupId>
            <artifactId>cas-client-core</artifactId>
            <version>${cas.version}</version>
        </dependency>

        <!--hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.4.7</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
        </plugins>
    </build>

</project>

说明:

spring-configuration-metadata.json 文件在项目build时自动生成(前提pom.xml中配置了spring-boot-configuration-processor),这样打包之后,在application.yaml中就可出现属性配置提示信息。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
;