本文以编写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>