前言:
该篇教程描述如何搭建CAS5.3.x集群操作,由于官方文档并没有贴出集群搭建方案,所以本博主根据源码剖析解决该问题。
进入下面小程序可以体验效果:
文档并没有深入浅出说明原理,直接贴代码用于快速上手学习。
学习CAS5.x 推荐看 以下两个博主文章
此博主文章:https://blog.csdn.net/u010475041/article/category/7156505
此博主文章:狂飙的yellowcong的博客_CSDN博客-centos,单点登录,Docker学习领域博主
全部基于springboot开发,请存在此基础再学习!
该教程由本博主(Garc)首发,所以转载请说明原处,谢谢!!
该教程分为两个部分,衔接第二篇
1.解决 flowExecutionKey CAS点击按钮后异常跳回原页面
PS:请按顺序一步一步来
如果发现CAS不能打印info日志,增加一个AsyncLogger指定自己的工程package就好了
以下所有的 configuration 都需要配置spring ,使用spring aop 配置
配置文件目录:src/main/resources/META-INF/spring.factories 增加如下内容:
com.hpay.sso.support.auth.config.CasWebflowContextConfiguration
解决该异常说明:
由于登录的时候在Nginx分发节点时,将webflow的其中一个节点的flowExecutionKey下发到另一个节点中,然后另一个节点不能正常解密(原因未找到,加密算法我没看懂)。导致出现CAS点击按钮后异常跳回原页面的问题,由于是nginx分发,所以会随机出现。
一、解决 flowExecutionKey 异常跳回原页面
maven依赖包
<dependency>
<groupId>org.cryptacular</groupId>
<artifactId>cryptacular</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-throttle-core</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-pm-webflow</artifactId>
<version>${cas.version}</version>
</dependency>
application.properties 配置
因为需要替换掉框架内的源码,所以必须要使用该配置排除框架的Bean配置管理使用自定义配置
spring.autoconfigure.exclude=org.apereo.cas.web.flow.config.CasWebflowContextConfiguration
EncryptedTranscoder 自定义加密解密算法
该代码是在原框架代码上加上了自定义加密解密算法,本人使用AES加密解密
AESCoder 此处不提供,请自备!
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import com.hpay.sso.support.auth.Crypto.AESCoder;
import org.apereo.spring.webflow.plugin.Transcoder;
import org.cryptacular.bean.BufferedBlockCipherBean;
import org.cryptacular.bean.CipherBean;
import org.cryptacular.bean.KeyStoreFactoryBean;
import org.cryptacular.generator.sp80038a.RBGNonce;
import org.cryptacular.io.URLResource;
import org.cryptacular.spec.BufferedBlockCipherSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedTranscoder implements Transcoder {
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedTranscoder.class);
private CipherBean cipherBean;
private boolean compression = true;
public EncryptedTranscoder() throws IOException {
BufferedBlockCipherBean bufferedBlockCipherBean = new BufferedBlockCipherBean();
bufferedBlockCipherBean.setBlockCipherSpec(new BufferedBlockCipherSpec("AES", "CBC", "PKCS7"));
bufferedBlockCipherBean.setKeyStore(this.createAndPrepareKeyStore());
bufferedBlockCipherBean.setKeyAlias("aes128");
bufferedBlockCipherBean.setKeyPassword("changeit");
bufferedBlockCipherBean.setNonce(new RBGNonce());
this.setCipherBean(bufferedBlockCipherBean);
}
public EncryptedTranscoder(CipherBean cipherBean) throws IOException {
this.setCipherBean(cipherBean);
}
public void setCompression(boolean compression) {
this.compression = compression;
}
protected void setCipherBean(CipherBean cipherBean) {
this.cipherBean = cipherBean;
}
public byte[] encode(Object o) throws IOException {
if (o == null) {
return new byte[0];
} else {
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = null;
try {
if (this.compression) {
out = new ObjectOutputStream(new GZIPOutputStream(outBuffer));
} else {
out = new ObjectOutputStream(outBuffer);
}
out.writeObject(o);
} finally {
if (out != null) {
out.close();
}
}
try {
byte[] bytes = AESCoder.encryptToByteArray(outBuffer.toByteArray());
if (Arrays.equals(bytes, outBuffer.toByteArray())){
bytes = this.cipherBean.encrypt(outBuffer.toByteArray());
}
return bytes;
} catch (Exception var7) {
throw new IOException("Encryption error", var7);
}
}
}
public Object decode(byte[] encoded) throws IOException {
byte[] data;
try {
data = AESCoder.decryptToByteArray(encoded);
if (Arrays.equals(data, encoded)){
data = this.cipherBean.encrypt(encoded);
}
} catch (Exception var11) {
throw new IOException("Decryption error", var11);
}
ByteArrayInputStream inBuffer = new ByteArrayInputStream(data);
ObjectInputStream in = null;
Object var5;
try {
if (this.compression) {
in = new ObjectInputStream(new GZIPInputStream(inBuffer));
} else {
in = new ObjectInputStream(inBuffer);
}
var5 = in.readObject();
} catch (ClassNotFoundException var10) {
throw new IOException("Deserialization error", var10);
} finally {
if (in != null) {
in.close();
}
}
return var5;
}
protected KeyStore createAndPrepareKeyStore() {
KeyStoreFactoryBean ksFactory = new KeyStoreFactoryBean();
URL u = this.getClass().getResource("/etc/keystore.jceks");
ksFactory.setResource(new URLResource(u));
ksFactory.setType("JCEKS");
ksFactory.setPassword("changeit");
return ksFactory.newInstance();
}
}
WebflowExecutorFactory 自定义工厂
定义此处工厂是为了 将获取加密算法的对象替换成自定义加密算法对象,完成加密算法的替换工作
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.configuration.model.webapp.WebflowProperties;
import org.apereo.cas.configuration.model.webapp.WebflowSessionManagementProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.cas.web.flow.executor.WebflowCipherBean;
import org.apereo.spring.webflow.plugin.ClientFlowExecutionRepository;
import org.apereo.spring.webflow.plugin.Transcoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.conversation.impl.SessionBindingConversationManager;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.impl.FlowExecutionImplFactory;
import org.springframework.webflow.execution.FlowExecutionListener;
import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader;
import org.springframework.webflow.execution.repository.impl.DefaultFlowExecutionRepository;
import org.springframework.webflow.execution.repository.snapshot
.SerializedFlowExecutionSnapshotFactory;
import org.springframework.webflow.executor.FlowExecutor;
import org.springframework.webflow.executor.FlowExecutorImpl;
import java.io.IOException;
public class WebflowExecutorFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(WebflowExecutorFactory.class);
private final WebflowProperties webflowProperties;
private final FlowDefinitionRegistry flowDefinitionRegistry;
private final CipherExecutor webflowCipherExecutor;
private final FlowExecutionListener[] executionListeners;
public FlowExecutor build() {
return this.webflowProperties.getSession().isStorage() ? this.buildFlowExecutorViaServerSessionBindingExecution() : this.buildFlowExecutorViaClientFlowExecution();
}
private FlowExecutor buildFlowExecutorViaServerSessionBindingExecution() {
SessionBindingConversationManager conversationManager = new SessionBindingConversationManager();
WebflowSessionManagementProperties session = this.webflowProperties.getSession();
conversationManager.setLockTimeoutSeconds((int)Beans.newDuration(session.getLockTimeout()).getSeconds());
conversationManager.setMaxConversations(session.getMaxConversations());
FlowExecutionImplFactory executionFactory = new FlowExecutionImplFactory();
executionFactory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(this.executionListeners));
SerializedFlowExecutionSnapshotFactory flowExecutionSnapshotFactory = new SerializedFlowExecutionSnapshotFactory(executionFactory, this.flowDefinitionRegistry);
flowExecutionSnapshotFactory.setCompress(session.isCompress());
DefaultFlowExecutionRepository repository = new DefaultFlowExecutionRepository(conversationManager, flowExecutionSnapshotFactory);
executionFactory.setExecutionKeyFactory(repository);
return new FlowExecutorImpl(this.flowDefinitionRegistry, executionFactory, repository);
}
private FlowExecutor buildFlowExecutorViaClientFlowExecution() {
ClientFlowExecutionRepository repository = new ClientFlowExecutionRepository();
repository.setFlowDefinitionLocator(this.flowDefinitionRegistry);
try {
repository.setTranscoder(this.getWebflowStateTranscoder());
} catch (IOException e) {
LOGGER.error("异常",e);
}
FlowExecutionImplFactory factory = new FlowExecutionImplFactory();
factory.setExecutionKeyFactory(repository);
factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(new FlowExecutionListener[0]));
repository.setFlowExecutionFactory(factory);
factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(this.executionListeners));
return new FlowExecutorImpl(this.flowDefinitionRegistry, factory, repository);
}
private Transcoder getWebflowStateTranscoder() throws IOException {
try {
WebflowCipherBean cipherBean = new WebflowCipherBean(this.webflowCipherExecutor);
return new EncryptedTranscoder(cipherBean);
} catch (Throwable var2) {
throw var2;
}
}
public WebflowExecutorFactory(WebflowProperties webflowProperties, FlowDefinitionRegistry flowDefinitionRegistry, CipherExecutor webflowCipherExecutor, FlowExecutionListener[] executionListeners) {
this.webflowProperties = webflowProperties;
this.flowDefinitionRegistry = flowDefinitionRegistry;
this.webflowCipherExecutor = webflowCipherExecutor;
this.executionListeners = executionListeners;
}
}
替换原框架CasWebflowContextConfiguration配置
需要该配置生效必须要配置spring.autoconfigure.exclude 排除掉源码的配置
如果有使用登录界面自定义参数(验证码之类)的,再其他地方配置了 FlowDefinitionRegistry loginFlowRegistry
请将那个配置在CasWebflowContextConfiguration之前加载!
import com.hpay.sso.support.auth.util.cipher.WebflowExecutorFactory;
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.util.CollectionUtils;
import org.apereo.cas.web.flow.CasFlowHandlerAdapter;
import org.apereo.cas.web.flow.CasWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowExecutionPlan;
import org.apereo.cas.web.flow.CasWebflowExecutionPlanConfigurer;
import org.apereo.cas.web.flow.actions.CasDefaultFlowUrlHandler;
import org.apereo.cas.web.flow.actions.LogoutConversionService;
import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.DefaultLogoutWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.GroovyWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.plan.DefaultCasWebflowExecutionPlan;
import org.apereo.cas.web.support.AuthenticationThrottlingExecutionPlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.webflow.config.FlowBuilderServicesBuilder;
import org.springframework.webflow.config.FlowDefinitionRegistryBuilder;
import org.springframework.webflow.context.servlet.FlowUrlHandler;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.ViewFactoryCreator;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
import org.springframework.webflow.execution.FlowExecutionListener;
import org.springframework.webflow.executor.FlowExecutor;
import org.springframework.webflow.expression.spel.WebFlowSpringELExpressionParser;
import org.springframework.webflow.mvc.builder.MvcViewFactoryCreator;
import org.springframework.webflow.mvc.servlet.FlowHandlerAdapter;
import org.springframework.webflow.mvc.servlet.FlowHandlerMapping;
import java.util.ArrayList;
import java.util.List;
@Configuration("casWebflowContextConfiguration")
@EnableConfigurationProperties({CasConfigurationProperties.class})
public class CasWebflowContextConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(CasWebflowContextConfiguration.class);
private static final int LOGOUT_FLOW_HANDLER_ORDER = 3;
private static final String BASE_CLASSPATH_WEBFLOW = "classpath*:/webflow";
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("authenticationThrottlingExecutionPlan")
private ObjectProvider<AuthenticationThrottlingExecutionPlan> authenticationThrottlingExecutionPlan;
@Autowired
@Qualifier("registeredServiceViewResolver")
private ObjectProvider<ViewResolver> registeredServiceViewResolver;
@Autowired
private ApplicationContext applicationContext;
@Autowired
@Qualifier("webflowCipherExecutor")
private CipherExecutor webflowCipherExecutor;
public CasWebflowContextConfiguration() {
}
@Bean
public ExpressionParser expressionParser() {
return new WebFlowSpringELExpressionParser(new SpelExpressionParser(), this.logoutConversionService());
}
@Bean
public ConversionService logoutConversionService() {
return new LogoutConversionService();
}
@RefreshScope
@Bean
public ViewFactoryCreator viewFactoryCreator() {
MvcViewFactoryCreator resolver = new MvcViewFactoryCreator();
resolver.setViewResolvers(CollectionUtils.wrap((ViewResolver)this.registeredServiceViewResolver.getIfAvailable()));
return resolver;
}
@Bean
public FlowUrlHandler loginFlowUrlHandler() {
return new CasDefaultFlowUrlHandler();
}
@Bean
public FlowUrlHandler logoutFlowUrlHandler() {
CasDefaultFlowUrlHandler handler = new CasDefaultFlowUrlHandler();
handler.setFlowExecutionKeyParameter("RelayState");
return handler;
}
@RefreshScope
@Bean
public HandlerAdapter logoutHandlerAdapter() {
FlowHandlerAdapter handler = new CasFlowHandlerAdapter("logout");
handler.setFlowExecutor(this.logoutFlowExecutor());
handler.setFlowUrlHandler(this.logoutFlowUrlHandler());
return handler;
}
@RefreshScope
@Bean
public FlowBuilderServices builder() {
FlowBuilderServicesBuilder builder = new FlowBuilderServicesBuilder();
builder.setViewFactoryCreator(this.viewFactoryCreator());
builder.setExpressionParser(this.expressionParser());
builder.setDevelopmentMode(this.casProperties.getWebflow().isRefresh());
return builder.build();
}
@Bean
public HandlerAdapter loginHandlerAdapter() {
FlowHandlerAdapter handler = new CasFlowHandlerAdapter("login");
handler.setFlowExecutor(this.loginFlowExecutor());
handler.setFlowUrlHandler(this.loginFlowUrlHandler());
return handler;
}
@RefreshScope
@Bean
@ConditionalOnMissingBean(
name = {"localeChangeInterceptor"}
)
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor bean = new LocaleChangeInterceptor();
bean.setParamName(this.casProperties.getLocale().getParamName());
return bean;
}
@Bean
public HandlerMapping logoutFlowHandlerMapping() {
FlowHandlerMapping handler = new FlowHandlerMapping();
handler.setOrder(3);
handler.setFlowRegistry(this.logoutFlowRegistry());
Object[] interceptors = new Object[]{this.localeChangeInterceptor()};
handler.setInterceptors(interceptors);
return handler;
}
@Lazy
@Bean
public Object[] loginFlowHandlerMappingInterceptors() {
List interceptors = new ArrayList();
interceptors.add(this.localeChangeInterceptor());
AuthenticationThrottlingExecutionPlan plan = (AuthenticationThrottlingExecutionPlan)this.authenticationThrottlingExecutionPlan.getIfAvailable();
if (plan != null) {
interceptors.addAll(plan.getAuthenticationThrottleInterceptors());
}
return interceptors.toArray();
}
@Bean
public HandlerMapping loginFlowHandlerMapping() {
FlowHandlerMapping handler = new FlowHandlerMapping();
handler.setOrder(2);
handler.setFlowRegistry(this.loginFlowRegistry());
handler.setInterceptors(this.loginFlowHandlerMappingInterceptors());
return handler;
}
@Bean
public FlowDefinitionRegistry logoutFlowRegistry() {
FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.builder());
builder.setBasePath("classpath*:/webflow");
builder.addFlowLocationPattern("/logout/*-webflow.xml");
return builder.build();
}
@Bean
public FlowDefinitionRegistry loginFlowRegistry() {
FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.builder());
builder.setBasePath("classpath*:/webflow");
builder.addFlowLocationPattern("/login/*-webflow.xml");
return builder.build();
}
@RefreshScope
@Bean
public FlowExecutor logoutFlowExecutor() {
WebflowExecutorFactory factory = new WebflowExecutorFactory(this.casProperties.getWebflow(), this.logoutFlowRegistry(), this.webflowCipherExecutor, new FlowExecutionListener[0]);
return factory.build();
}
@RefreshScope
@Bean
public FlowExecutor loginFlowExecutor() {
WebflowExecutorFactory factory = new WebflowExecutorFactory(this.casProperties.getWebflow(), this.loginFlowRegistry(), this.webflowCipherExecutor, new FlowExecutionListener[0]);
return factory.build();
}
@ConditionalOnMissingBean(
name = {"defaultWebflowConfigurer"}
)
@Bean
@Order(0)
@RefreshScope
public CasWebflowConfigurer defaultWebflowConfigurer() {
DefaultLoginWebflowConfigurer c = new DefaultLoginWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
c.setOrder(-2147483648);
return c;
}
@ConditionalOnMissingBean(
name = {"defaultLogoutWebflowConfigurer"}
)
@Bean
@Order(0)
@RefreshScope
public CasWebflowConfigurer defaultLogoutWebflowConfigurer() {
DefaultLogoutWebflowConfigurer c = new DefaultLogoutWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
c.setOrder(-2147483648);
return c;
}
@ConditionalOnMissingBean(
name = {"groovyWebflowConfigurer"}
)
@Bean
@DependsOn({"defaultWebflowConfigurer"})
@RefreshScope
public CasWebflowConfigurer groovyWebflowConfigurer() {
GroovyWebflowConfigurer c = new GroovyWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
return c;
}
@Autowired
@Bean
public CasWebflowExecutionPlan casWebflowExecutionPlan(List<CasWebflowExecutionPlanConfigurer> configurers) {
DefaultCasWebflowExecutionPlan plan = new DefaultCasWebflowExecutionPlan();
configurers.forEach((c) -> {
c.configureWebflowExecutionPlan(plan);
});
plan.execute();
return plan;
}
@ConditionalOnMissingBean(
name = {"casDefaultWebflowExecutionPlanConfigurer"}
)
@Bean
public CasWebflowExecutionPlanConfigurer casDefaultWebflowExecutionPlanConfigurer() {
return new CasWebflowExecutionPlanConfigurer() {
public void configureWebflowExecutionPlan(CasWebflowExecutionPlan plan) {
plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.defaultWebflowConfigurer());
plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.defaultLogoutWebflowConfigurer());
plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.groovyWebflowConfigurer());
}
};
}
}
全部教程到此结束
第三篇教程内容到此已经结束了,只要根据此教程一步一步操作,就可以实现集群登录操作。
如果想了解更多,可以加QQ群 119170668